67 Commits
Author SHA1 Message Date
Djeex bf97cac2e9 Better writing 2026-09-05 22:10:30 +02:00
Djeex 9a354ee62a Fix the 301 redirects to match the production URL scheme 2026-09-05 17:55:36 +02:00
Djeex 2e041bcfb0 Complete the French mirror of the remaining articles 2026-09-05 17:55:36 +02:00
Djeex 1894ac7ead Mirror the French docs onto the English structure 2026-09-05 16:55:39 +02:00
Djeex 86f04ed794 Rewrite the Docker article intro 2026-09-05 12:53:29 +02:00
Djeex c884432594 Wrap command names in headings as inline code 2026-09-05 12:48:46 +02:00
Djeex bc87e7b7e3 Reference ufw in the summary table and install command 2026-09-05 12:45:35 +02:00
Djeex 34e4beb0d7 Add an rm confirmation guard article and a ufw guide 2026-09-05 12:42:58 +02:00
Djeex 142788d740 Add a Linux tips section and move Docker stacks to /srv/docker 2026-09-05 12:28:05 +02:00
Djeex 11d6c275c8 Rewrite the Debian installation article 2026-09-05 11:36:14 +02:00
Djeex ebf65c247c Prerender robots.txt so the static build keeps its sitemap reference 2026-09-04 23:02:50 +02:00
Djeex 90eb205e85 Fix the production build and restore directory-style output 2026-09-04 22:56:13 +02:00
Djeex 942f87dcac Convert the Authentik database migration section to steps 2026-09-04 19:03:39 +02:00
Djeex 9c5a693281 Add section summary pages and make single-link admonitions clickable 2026-09-04 18:51:42 +02:00
Djeex f2cfa49150 Restore the landing page og:image and update the README tagline 2026-09-04 17:50:44 +02:00
Djeex e82dbafd4a Add Roboto and the site logo to the OG image template 2026-09-04 17:32:59 +02:00
Djeex fd830796e8 Restyle the docs OG image and fix the landing page social image 2026-09-04 17:16:59 +02:00
Djeex 1a8cf21c88 Show the other-projects links at the bottom of the docs ToC 2026-09-04 16:58:48 +02:00
Djeex b401a678cb Add Arcane as an advanced Dockge alternative 2026-09-04 16:54:05 +02:00
Djeex 1337fae991 Rework the Serveex intro page and fix icon colors 2026-09-04 16:33:37 +02:00
Djeex a7b3880088 Convert the remaining Serveex and recycled articles to steps 2026-09-04 15:57:38 +02:00
Djeex cd75fd2288 Convert Nextcloud and Pingvin to steps 2026-09-04 15:14:12 +02:00
Djeex a7b85af45a Remove default icon from tip blocks that already start with an emoji 2026-09-04 14:56:09 +02:00
Djeex eeeaa706a2 Convert Immich installation and SWAG exposure sections to steps 2026-09-04 14:56:03 +02:00
Djeex b8fbab8a18 Remove a stale comment in FileTreeNode and update the arr diagram 2026-09-04 14:36:00 +02:00
Djeex a8b9f8f5db Stop exposing Jellyfin publicly and document WireGuard remote access 2026-09-04 14:35:49 +02:00
Djeex 8b5e61de66 Convert servarr to steps and add TinyAuth protection for Seerr 2026-09-04 14:35:38 +02:00
Djeex f4dc53d3cf Convert the qbittorrent sections to the steps component 2026-09-04 14:35:26 +02:00
Djeex 5b4d8732e0 Convert beszel and upsnap install/expose sections to the steps component 2026-09-04 14:35:17 +02:00
Djeex 5114e8ae87 Link Authentik by name in the TinyAuth-alternative tips across 7 files 2026-09-04 14:35:06 +02:00
Djeex c70f26bb0e Remove stray blank lines after comments in compose snippets 2026-09-02 23:46:26 +02:00
Djeex 8b2073474d Remove stray blank lines and start every stack with --- 2026-09-02 23:42:00 +02:00
Djeex 8ca6bec943 Fix FileTree guide-line alignment and folder icon connections 2026-09-02 23:34:13 +02:00
Djeex de2861fa78 Document the FileTree component in CUSTOMIZATIONS.md 2026-09-02 23:02:58 +02:00
Djeex 54edbae731 Add a FileTree component and convert every directory tree to it 2026-09-02 23:02:17 +02:00
Djeex 940096b2b1 Fix canonical URL and sitemap trailing slash consistency 2026-09-02 20:18:23 +02:00
Djeex 0b429f5465 Deprecate File Browser and consolidate the archives into Alternatives 2026-09-02 19:28:47 +02:00
Djeex 2e70319d99 Point the File Browser links to File Browser Quantum 2026-09-02 19:28:37 +02:00
Djeex dd95694492 Add native Pocket ID OIDC tutorials across the app guides 2026-09-02 19:28:29 +02:00
Djeex bb8581a535 Add step-by-step TinyAuth protection sections across the guides 2026-09-02 19:28:22 +02:00
Djeex 1117ff1827 Recommend TinyAuth alongside Authentik in exposure warnings 2026-09-02 18:16:50 +02:00
Djeex b9580e2e5f Add a dedicated TinyAuth section to Uptime-Kuma 2026-09-02 18:16:40 +02:00
Djeex 59f9d5dbc7 Remove the Authentik comparisons from Pocket ID and TinyAuth 2026-09-02 18:16:30 +02:00
Djeex b864251f48 Convert step-by-step sections to the steps component 2026-09-02 18:16:20 +02:00
Djeex 11c5432955 Replace Gitea with Forgejo in the development section 2026-08-31 23:44:50 +02:00
Djeex 60712f5499 Archive Gitea in Recycled before switching to Forgejo 2026-08-31 23:44:40 +02:00
Djeex 9abee89478 Remove em dashes from the remaining English content 2026-08-31 23:39:19 +02:00
Djeex 4090203dc4 Add Pocket ID, TinyAuth and File Browser Quantum 2026-08-31 23:39:07 +02:00
Djeex 91ea3f9a70 Switch the media stack from Plex to Jellyfin and Overseerr to Seerr 2026-08-31 23:38:40 +02:00
Djeex 81df3351f5 Archive the Plex media stack and reorganize the security section 2026-08-31 23:38:20 +02:00
Djeex 6696ed9b23 Name and icon code blocks by real filename across the site 2026-08-31 22:12:47 +02:00
Djeex baf3590186 Label terminal commands and directory trees in code blocks 2026-08-31 19:57:52 +02:00
Djeex 61f4b0447b Add MIT license and note it in CUSTOMIZATIONS.md 2026-08-31 19:24:51 +02:00
Djeex b18c218b4e Add MIT license and remove the one-off content migration scripts 2026-08-31 19:24:39 +02:00
Djeex e32fe6ccf3 Rewrite the README with real setup instructions 2026-08-31 19:22:38 +02:00
Djeex 4c76897cac Revamping articles 2026-08-30 23:54:44 +02:00
Djeex 5497a5cdc4 Merge the documentation disclaimer into a single paragraph 2026-08-30 23:54:02 +02:00
Djeex 8caa19b132 Add sparkle emoji and hide the icon consistently on tip admonitions 2026-08-30 23:34:09 +02:00
Djeex 588139bfa7 Restructure the Samba tutorial with steps components 2026-08-30 23:33:59 +02:00
Djeex 21a5eb670e Add CUSTOMIZATIONS.md documenting everything added over base Docus 2026-08-30 23:33:52 +02:00
Djeex 022a9d96bd Remove decorative dashes and fix two broken admonitions 2026-08-30 19:53:57 +02:00
Djeex bb1a482a8d Switch editing instructions from vim to nano and use kbd for keys 2026-08-30 18:49:10 +02:00
Djeex 2608bf64eb Match admonition colors to the original site and allow hiding their icon 2026-08-30 17:38:13 +02:00
Djeex 04268535a6 Close a warning admonition swallowing the rest of raid.md 2026-08-30 17:04:48 +02:00
Djeex 630e8b9c84 Show page contributors from git history, linked to Gitea 2026-08-30 17:01:34 +02:00
Djeex ab8671e0fb Add legacy French URL redirects and fix header click-through bug 2026-08-30 16:46:28 +02:00
Djeex c51fcd5df6 Migrate docudjeex to Docus v4 with EN/FR content 2026-08-30 16:41:37 +02:00
262 changed files with 39439 additions and 20703 deletions
-14
View File
@@ -1,14 +0,0 @@
module.exports = {
root: true,
extends: ['@nuxt/eslint-config'],
ignorePatterns: [
'dist',
'node_modules',
'.output',
'.nuxt'
],
rules: {
'vue/max-attributes-per-line': 'off',
'vue/multi-word-component-names': 'off'
}
}
Executable → Regular
+43 -11
View File
@@ -1,12 +1,44 @@
node_modules # Nuxt dev/build outputs
*.iml
.idea
*.log*
.nuxt
.vscode
.DS_Store
coverage
dist
sw.*
.env
.output .output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
.eslintcache
# Local env files
.env
.env.*
!.env.example
# npm pack
*.tgz
# Temp files
.tmp
.profile
*.0x
#VSC
.history
.wrangler
# Python
__pycache__
*.pyc
# Scratch/demo files (not part of the site)
scratch
-1
View File
@@ -1 +0,0 @@
strict-peer-dependencies=false
+79
View File
@@ -0,0 +1,79 @@
# Customizations over base Docus
This project starts from the `docus` i18n starter template (`extends: ['docus']` in `nuxt.config.ts`, Docus v5.x on Nuxt ^4.4.8). This file tracks everything added or changed on top of that base, and *why*, so a future contributor doesn't have to diff `node_modules/docus` to find out.
## Packages
- **`better-sqlite3`** — Nuxt Content v3 stores all parsed markdown content in a local SQLite database (`.data/content/contents.sqlite`) using its own DB layer (`db0`) rather than reading files at request time. `db0` needs an actual SQLite driver to talk to that file, and lists `better-sqlite3` as a *peer* dependency (alongside alternatives like `sqlite3` or `@libsql/client`) — peer dependencies aren't auto-installed by npm, so without declaring it explicitly, `@nuxt/content` has no driver to write to and the local content database silently fails to build.
- **`@nuxtjs/i18n`** module added explicitly in `nuxt.config.ts`. The starter ships an i18n-*shaped* content structure (`content/en/`, `content/fr/`) out of the box, but that's just a folder convention — nothing routes `/fr/...` URLs, switches locales, or auto-detects the browser's language unless the module itself is registered.
## `nuxt.config.ts`
- **Git-based page contributors.** `getContributors()` runs `git log --format=%an --follow -- <file>` for each markdown file and dedupes the author list, injected into the page's content via the `content:file:afterParse` hook. This was chosen over the Gitea/GitHub API because it needs no access token, no network call, and no rate limiting — the info is already in the checkout. The trade-off: CI must do a **full** (non-shallow) `git checkout`, otherwise `git log` only sees one commit per file and every page shows just its most recent author instead of everyone who ever touched it.
- **`@nuxt/image` dev/prod path branch.** `@nuxt/image` resolves where to read local files from differently depending on context: in `nuxi dev` it needs an absolute filesystem path to `public/`, but in a production build it needs the plain relative string `'public/'` — passing the absolute path there breaks the `Content-Type` header on the built `/_ipx` image-proxy route specifically for SVGs (they'd get served with the wrong MIME type). The `isDev` check branches on the actual `nuxi` subcommand so both environments get the value they each require.
- **Custom icon collection.** `icon.customCollections` registers a `brand` prefix pointing at `app/assets/brand-icons/`, so logos for the user's other projects (Instameex, Lumeex) can be referenced from content as `i-brand-instameex` etc., exactly like any Iconify icon — without needing to publish them to an actual Iconify icon set first.
- **Markdown highlight.** Forces the `github-dark` Shiki theme for *both* the light and dark slots, because the site never actually offers a light mode (see `docus.colorMode: 'dark'` below) — maintaining two highlight themes for a mode nobody sees would just be dead config. The extra languages (`nginx, properties, php, toml, console, sh, yaml`) were added because the tutorial content includes config-file snippets and terminal output in all of these syntaxes, and none of them are in Shiki's minimal default bundle for Nuxt Content.
- **`darkreader-lock` meta tag.** The Dark Reader browser extension rewrites elements' inline `style` attributes on the client, after Nuxt has already server-rendered them — so any component using an inline `style` (like the cyan "·" separator spans) ends up with mismatched HTML between server and client, and Vue logs a hydration-mismatch warning on every page load for any visitor running that extension. This meta tag is Dark Reader's own opt-out signal, telling the extension to leave the page alone instead of trying to work around the mismatch after the fact.
- **301 redirects (`routeRules`).** The old site served French content at root-level URLs (e.g. `/generalites/reseau/nat`, no locale prefix, on a separate `french` git branch). Restructuring into a single repo with `@nuxtjs/i18n`'s `/fr/...` prefix changed every French URL, which would otherwise break external links, bookmarks, and search-engine rankings for those pages built up over time. All 44 mappings use `statusCode: 301` explicitly — Nitro's default redirect status is 307 (temporary), which search engines don't treat as "please re-index this at the new URL" the way a 301 (permanent) does. There's deliberately **no** `/``/fr` redirect: root already serves English by default, and `@nuxtjs/i18n`'s `detectBrowserLanguage` already handles sending French-browser visitors to `/fr` automatically — a static redirect rule would just fight with that.
- **`site.trailingSlash: true`.** The site builds as a static export (`nuxt build`, deployed as static files on a web server) and the production host 301-redirects a bare `path` request to `path/` (verified against `docu.djeex.fr`), so canonical/og:url/sitemap URLs need to already carry the trailing slash — otherwise the canonical tag points at the very URL the server redirects away from, a loop that keeps the page out of search results. This is documented, official behavior for the wider Nuxt SEO ecosystem (`nuxtseo.com`'s "Trailing Slashes" guide), but Docus doesn't depend on `nuxt-seo-utils` for its canonical/og:url logic — it hand-rolls its own in `useSeo.ts` via a plain `joinURL(site.url, route.path)` that never checks this setting. That gap is why the items below exist alongside it.
- **`nitro.prerender.autoSubfolderIndex: true`.** Docus sets this to `false` in its own `nuxt.config.ts`, which writes every route as `path.html` instead of `path/index.html` — the exact opposite of what the trailing-slash setup above needs, since the host would then redirect `/path` to `/path/` and find no directory there. Restoring the Nitro default puts the files back where the advertised URLs actually point.
- **`nitro.prerender.routes: ['/', '/robots.txt']`.** Docus's `nitro:config` hook seeds one prerender route per locale (`/en`, `/fr`) and `/sitemap.xml`, but never `/` or `/robots.txt` — both exist as server routes yet were never written to the static output, so both 404 on a static host. `/` loses i18n's redirect to the default locale, and robots.txt loses the `Sitemap:` line that points crawlers at the sitemap (the live site has this gap today: requesting `/robots.txt` returns the site's HTML). Prerendering them is all that's needed. (Deliberately not a `routeRules` redirect for `/`: a hard redirect there would override i18n's `detectBrowserLanguage`, which is what sends French-browser visitors to `/fr`.)
- **`experimental.defaults.nuxtLink.trailingSlash: 'append'`.** The native Nuxt-core (not `@nuxtjs/i18n`'s own, separate `trailingSlash` option — that one only affects `switchLocalePath()` and double-appends the slash on hreflang alternate links) way to make every `<NuxtLink>` href, including the ones i18n's `switchLocalePath` builds for hreflang tags, resolve with a trailing slash already, matching both `site.trailingSlash` and the directory-style files on disk.
> **Do not add a global trailing-slash redirect middleware here.** An earlier revision had `app/middleware/trailing-slash.global.ts` 301-redirecting bare paths to their slash form. It broke the production build outright: Nitro's prerender crawler seeds on `/en` and `/fr`, the middleware turned both into redirect responses, and since Nitro extracts no links from a redirect the crawl stopped immediately — 31 routes and 22 HTML files instead of 557 and 146, with every content page missing. The host already performs that redirect server-side, so the middleware bought nothing.
## `server/routes/sitemap.xml.ts`
Overrides Docus's own `sitemap.xml` route (`node_modules/docus/server/routes/sitemap.xml.ts`), for two reasons:
- Docus's version resolves the site URL via `inferSiteURL()`, which only reads deployment-platform env vars (Vercel/Netlify/Cloudflare Pages, or `NUXT_PUBLIC_SITE_URL`/`NUXT_SITE_URL`) — never the `site.url` set in this project's `nuxt.config.ts`. In `nuxt dev` none of those env vars exist, so every `<loc>` came out as a bare relative path instead of an absolute URL, which is invalid per the sitemap spec.
- Even where that env var happens to be set, Docus's version builds each `<loc>` with plain string concatenation and has no concept of `site.trailingSlash` at all, so it could never match the trailing-slash canonical/og:url above.
This override is otherwise a straight copy of Docus's route, with the URL-building swapped for `createSitePathResolver()` (from `nuxt-site-config`), which resolves from the same `site` config as canonical/og:url and honors `trailingSlash` correctly.
## `content.config.ts`
Nuxt Content validates every page's frontmatter against a Zod schema per collection, and **silently drops any key that isn't declared in that schema** — it doesn't error, the field just isn't there at render time. This file reimplements docus's own `createDocsSchema()` (not something the `docus` package actually exports, so it has to be copied rather than imported) and extends it with the custom frontmatter toggles the page template relies on:
- `hideHeader` — skip the title/description block on a page (used for pages that want a custom hero instead of the standard header).
- `hideCopyPage` — hide the "Copy page" button group (for pages where "copy as markdown for an LLM" doesn't make sense).
- `hideToc` — hide the right-hand table of contents (for short pages where a TOC would be mostly empty space).
- `contributors` — the array populated by the `getContributors()` hook above; without this line in the schema, the hook's output would be computed and then thrown away.
This was a real bug during development: `hideHeader`/`hideCopyPage` did nothing at all until this schema was extended, because the fields were being stripped before the page component ever saw them.
## `app/app.config.ts`
The old production site (`docu.djeex.fr`) has an established visual identity that a "generic Nuxt UI theme" migration would have lost. These overrides were measured directly against the live old site (colors picked from its actual computed styles, not eyeballed) so the new stack keeps the same look rather than just being *a* documentation theme:
- `docus.colorMode: 'dark'` — the old site never had a light mode either; hard-locking it here removes the need for the toggle UI and light-theme variants entirely, rather than half-supporting a mode nobody uses.
- `ui.colors`: primary `cyan`, neutral `zinc` — the site's brand accent color and its neutral gray scale.
- `ui.prose.card` / `ui.prose.pre` / `ui.header` / `ui.contentSearchButton` / `ui.contentSurround` / `ui.kbd`: exact background/border hex values (a shared `rgba(12,13,12,0.8)` translucent-dark family, e.g. `#121110` borders) matching the old site's card, code-block, header, search button, and prev/next-link chrome, since Nuxt UI's defaults use a different neutral scale that didn't match.
- `ui.prose.callout.compoundVariants`: exact colors for all four admonition severities (info/success/warning/error), overriding Nuxt UI's default callout palette so `::note`, `::tip`, `::warning`, `::caution` render in the same colors the old site's `::alert` boxes used, rather than Nuxt UI's stock blue/green/amber/red.
- `toc.bottom.links` / `toc.bottom.title` — no component override needed for this one: Docus's own `DocsAsideRightBottom.vue` already reads `appConfig.toc?.bottom?.links` and renders them via `UPageLinks` under the right-hand table of contents, it's just never set by default. This surfaces the same "other projects" links (git.djeex.fr, Lumeex, Instameex) shown on the landing page's "Other dumb things" section, at the bottom of every doc page's TOC too, instead of only being visible from the homepage.
## Custom / overridden components (`app/components/`)
Nuxt's convention is that a file at `app/components/<any-subfolder>/<ExactComponentName>.vue` overrides a layer's (here, docus's) auto-registered component of the same name — no explicit registration needed, just matching the filename. Each one below was diffed against the actual stock file in `node_modules/docus` to confirm it's a real, deliberate change and not an accidental untouched copy:
- **`app/AppHeader.vue`** — added a Gitea social icon link alongside the stock GitHub link. The project's canonical repository lives on the user's self-hosted Gitea instance; GitHub is only a mirror, so a GitHub-only link would point visitors to the secondary copy.
- **`app/AppHeaderCenter.vue`** — the most heavily rewritten component. Stock Docus sizes the header's nav menu to the header's own container width, but this site's actual docs pages use a narrower, off-center content column (a two-level 10-column grid: an outer sidebar column plus an inner article/TOC split) — so the stock menu didn't visually line up under the content it was supposed to sit above. This override renders the nav as an absolutely-positioned overlay that replicates that exact two-level grid, so it lines up with the real article column instead of the header's own slot. Also fixes a real bug found during development: `pointer-events-auto` was originally applied to the full-width wrapper div, which silently blocked clicks on the logo and the right-side icons (search, color mode, socials) everywhere *except* the homepage (a different code path with an empty nav). It's now scoped to only the innermost column div that actually contains clickable content.
- **`app/AppHeaderBottom.vue`** — emptied to a no-op `<div />`. Once navigation moved into `AppHeaderCenter` above, the stock second nav row would have shown the same links twice and wasted vertical space in the header.
- **`docs/DocsAsideLeftBody.vue`** — the left doc-tree sidebar is now collapsible and closed by default (stock: always fully expanded, not collapsible). With this site's number of nested sections, a fully-expanded tree was one very long scrollable list on every page load; collapsed-by-default lets a visitor see the top-level structure first and open only the section they need.
- **`docs/DocsAsideLeftTop.vue`** — added a full-width search button above the sidebar for the header-based subnav mode (stock rendered nothing there in that mode, only in the "aside" subnav mode). Without it, visitors on pages using header-mode subnav had no visible way to open search from the sidebar area at all.
- **`docs/DocsPageHeaderLinks.vue`** — gave the "Copy page" button group the same translucent-dark card styling used everywhere else on the site. Purely cosmetic: the stock Nuxt UI button styling didn't match the rest of the page chrome and stood out as an unstyled default.
- **`prose/ProseNote.vue`, `ProseTip.vue`, `ProseWarning.vue`, `ProseCaution.vue`** (new files, no stock equivalent to override against — these are thin wrappers around Nuxt UI's own `Callout.vue`). Nuxt UI's admonition icon is normally set once, globally, per icon slot — there's no built-in way to omit it on just one specific admonition without changing it for every admonition of that type site-wide. These wrappers read an optional `icon` prop so a single instance can hide its icon (`::note{icon=""}`) when the emoji or leading text already conveys the same meaning, while every other `::note` on the site keeps its default icon.
- **`content/Ellipsis.vue`** (new; no Docus or Nuxt UI equivalent exists at all). The old site had a decorative blurred gradient glow behind section headers, and reproducing the content 1:1 meant this cosmetic effect needed *some* markdown-usable component to exist, since neither Docus nor Nuxt UI ships anything similar. Registered as the inline MDC component `:ellipsis{left= width= top= blur= zIndex=}`, used across content wherever the old site had that effect.
- **`OgImage/Docs.takumi.vue`** — overrides Docus's default `og:image` template used for every doc page's social-preview image. Stock Docus renders it on a generic `bg-neutral-950` with a plain white corner flare, in whatever font the takumi renderer defaults to; this swaps in the site's actual near-black background (`#0B0A0A`, matching `app.css`), a blurred oval reproducing the exact colors and diagonal gradient of the site's own `:ellipsis` component instead of the white flare, **Roboto** as the font (the site itself renders in the browser's own `system-ui`, which can't be embedded server-side since it resolves to a different, non-redistributable font per OS — Roboto was picked as Android's system font, the single most common one), and the site's own logo (bottom-left) in place of the plain site-name text. Two non-obvious takumi rendering gotchas found in the process: an injected SVG's XML prolog and comments render as literal visible text instead of being silently ignored like a browser's `innerHTML` would, and a `<style>` block's CSS class rules aren't resolved at all (paths fell back to default black fill) — both needed stripping/inlining by hand in `fetchLogoSvg()` before the SVG string reaches `v-html`. `content/en/index.md` and `content/fr/index.md` skip this template entirely via the `seo.ogImage` frontmatter key (Docus's `landing.vue` checks for it and falls back to a fixed `/img/social.png` instead of generating one), since the homepage's own hero doesn't fit this per-doc-page layout.
- **`content/FileTree.vue` + `content/FileTreeNode.vue`** (new; no Docus or Nuxt UI equivalent exists at all). Every install guide used to show its folder layout as a plain ASCII-art code fence (`└──`/`├──`); this renders the same information as an actual tree with per-entry folder/file icons instead, reusing the exact filename/extension icon lookup `CodeIcon.vue` already does for labeled code fences, so a `.env` or `.conf` gets the same icon here as in a fence header. Registered as the container component `::file-tree`, fed through a YAML props block (`remark-mdc`'s `---\n...\n---` syntax) rather than a nested markdown list, since the data (name, whether it's a folder, its children) doesn't map cleanly onto list semantics otherwise. A trailing `/` on a plain string marks an otherwise-childless folder (a mapping key is unambiguously a folder already); a trailing `" # comment"` on either form renders as a dimmed, italic aside, matching a real code comment without being one (an actual unquoted YAML `#` would just be stripped by the parser before the component ever saw it). The header doubles as a collapse toggle (`collapsed` prop sets the initial state only), and clicking any row copies that entry's full path to the clipboard.
## Page-level features (`app/pages/[[lang]]/[...slug].vue`)
This catch-all page isn't a docus override (docus doesn't ship one to override — this project defines its own), but it layers frontmatter-driven behavior on top of stock Nuxt Content rendering:
- `hideHeader` / `hideCopyPage` / `hideToc` — read the three frontmatter toggles declared in `content.config.ts` above and conditionally skip rendering each block.
- **Contributors + history block.** Below the "Edit this page" / "Report an issue" links, renders "Contributor(s): <names>" from the `contributors` frontmatter field (populated by the git-log hook), with the names linking to that specific file's Gitea commit history. The goal is to give credit to everyone who's worked on a page — not just whoever last edited it — and let a reader jump straight to the full history of a page without leaving the site or knowing the underlying file path.
## License
MIT (see `LICENSE`), same as the Docus theme this project is built on.
+8 -1
View File
@@ -1,5 +1,6 @@
MIT License MIT License
Copyright (c) 2025 > Djeex
Copyright (c) 2026 Djeex
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal
@@ -18,3 +19,9 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. SOFTWARE.
---
This project is built on the Docus theme (https://docus.dev), also MIT
licensed (Copyright (c) the Docus authors), which is compatible with and
distributed under the same terms above.
Executable → Regular
+45 -19
View File
@@ -1,43 +1,69 @@
<p align="center"> <p align="center">
<img src="https://git.djeex.fr/Djeex/DjeexLab/raw/branch/main/docs/files/img/global/lab.svg" align="center" width="700"> <img src="public/img/global/docudjeex-home.svg" align="center" width="700">
[![docu.djeex.fr](https://img.shields.io/badge/Docu·djeex-00b0f0?style=for-the-badge&logoColor=white&logo=materialformkdocs)](https://docu.djeex.fr/) [![docu.djeex.fr](https://img.shields.io/badge/Docu·djeex-00b0f0?style=for-the-badge&logoColor=white&logo=materialformkdocs)](https://docu.djeex.fr/)
[![Uptime-Kuma](https://stats.djeex.fr/api/badge/23/status?style=for-the-badge)](https://docu.djeex.fr/) [![Uptime-Kuma](https://stats.djeex.fr/api/badge/23/status?style=for-the-badge)](https://docu.djeex.fr/)
</p> </p>
# 🔧 Docs, More Docs # 🔧 Homelab docs & other dumb things
**Docu·djeex** is first and foremost a personal project aimed at self-hosting as many everyday services as possible without relying on proprietary platforms (Google, Apple, Netflix, etc.). **Docu·djeex** is first and foremost a personal project aimed at self-hosting as many everyday services as possible, without relying on proprietary platforms (Google, Apple, Netflix, etc.).
This documentation site is built using [Nuxt.js](https://nuxt.com/). This documentation site is built using [Nuxt.js](https://nuxt.com/), on the [Docus](https://docus.dev) theme (Nuxt UI + Nuxt Content).
This repository contains everything you need to edit pages, apply your changes, and redeploy the site. This repository contains everything you need to edit pages, apply your changes, and redeploy the site. See [CUSTOMIZATIONS.md](CUSTOMIZATIONS.md) for everything added on top of the base Docus theme.
## Setup ## Requirements
- Node.js 20 or later
- npm
## Getting started
Install dependencies: Install dependencies:
```sh ```bash
npm install npm install
``` ```
## Development Environment (port 3000) Start the dev server:
```sh ```bash
npm run dev npm run dev
``` ```
## Generate Static Pages The site will be available at `http://localhost:3000`.
```sh ## Build
npm run generate
```bash
npm run build
``` ```
The HTML files will be generated in the `.output/public` folder and are ready to be deployed on any static-compatible hosting. This builds the production site (pointed at `https://docu.djeex.fr` via `NUXT_SITE_URL`) into `.output`. Run it with:
## Preview Build ```bash
node .output/server/index.mjs
If you'd like to immediately see the result of your static site build, you can launch a preview server:
```sh
npm run preview
``` ```
## Project structure
```
content/
├── en/ # English content, served at /en/...
└── fr/ # French content, served at /fr/...
app/
├── components/ # Custom components and overrides of Docus's own components
└── pages/ # The catch-all docs page
content.config.ts # Content collections and frontmatter schema
nuxt.config.ts # Nuxt/Docus/i18n configuration
app/app.config.ts # Theme, colors, branding
```
## Languages
- English (`en`) — default locale, served under `/en`
- French (`fr`) — served under `/fr`
Visiting `/` redirects to `/en` or `/fr` based on the visitor's browser language (or a previous choice, remembered via cookie).
-81
View File
@@ -1,81 +0,0 @@
// https://github.com/nuxt-themes/docus/blob/main/nuxt.schema.ts
export default defineAppConfig({
css: ['~/assets/css/extra.css'],
colorMode: {
preference: 'dark',
fallback:'dark',
},
content: {
highlight: {
langs: [
'console',
'nginx',
]
}
},
mdc: {
highlight: {
theme: 'github-dark',
langs: ['ts','console','nginx'],
wrapperStyle: true
}
},
docus: {
title: 'Docudjeex',
description: 'Homelab documentation',
url: 'https://docu.djeex.fr',
image: '/img/social.png',
socials: {
github:'',
Language: {
label: '🇫🇷',
icon:'material-symbols:language-french',
href: 'https://docu.djeex.fr/fr/',
},
Gitea: {
label: 'Gitea',
icon: 'cib:gitea',
href: 'https://git.djeex.fr/Djeex/docudjeex',
},
Github: {
label: 'Github',
icon:'cib:github',
href: 'https://github.com/Djeex',
}
},
github: {
baseUrl:'https://git.djeex.fr',
dir: 'content',
branch: 'src/branch/master',
repo: 'docudjeex',
owner: 'Djeex',
edit: false
},
aside: {
level: 0,
collapsed: false,
exclude: []
},
main: {
padded: true,
fluid: true
},
header: {
logo: true,
showLinkIcon: true,
exclude: [],
fluid: false
},
footer: {
credits: {
icon: '',
text: '',
href: '',
}
}
},
})
+140
View File
@@ -0,0 +1,140 @@
export default defineAppConfig({
docus: {
locale: 'en',
colorMode: 'dark',
},
navigation: {
sub: 'header',
},
header: {
title: 'Docudjeex',
logo: {
light: '/img/logo.svg',
dark: '/img/logo.svg',
alt: 'Docudjeex',
},
},
socials: {
gitea: 'https://git.djeex.fr/Djeex/docudjeex',
},
github: {
url: 'https://github.com/Djeex/docudjeex',
},
toc: {
bottom: {
title: 'Other dumb things',
links: [
{
icon: 'i-cib-gitea',
label: 'git.djeex.fr',
to: 'https://git.djeex.fr',
target: '_blank',
},
{
icon: 'i-brand-lumeex',
label: 'Lumeex',
to: 'https://lumeex.djeex.fr',
target: '_blank',
},
{
icon: 'i-brand-instameex',
label: 'Instameex',
to: 'https://instameex.djeex.fr',
target: '_blank',
},
],
},
},
ui: {
colors: {
primary: 'cyan',
neutral: 'zinc',
},
prose: {
card: {
slots: {
base: 'bg-[rgba(12,13,12,0.8)] border-[#121110]',
},
},
// Custom code-block header icons. Full labels (```text [Arborescence])
// match by exact lowercase filename; bare extensions (no filename
// match) fall back to matching any ```lang [*.ext] label.
codeIcon: {
'arborescence': 'i-lucide-folder-tree',
'directory tree': 'i-lucide-folder-tree',
'ini': 'i-lucide-settings',
'conf': 'i-lucide-settings',
'service': 'i-lucide-settings',
// Used as ::code-group tab labels when a command differs per OS.
'macos': 'i-simple-icons-apple',
'linux': 'i-simple-icons-linux',
'windows': 'i-simple-icons-windows',
},
pre: {
slots: {
base: 'bg-[#121110] border-[#201e1b] rounded-lg',
header: 'bg-[#121110] border-[#201e1b]',
},
},
// Exact colors measured on the old site's ::alert boxes (note=info,
// tip=success, warning=warning, caution=danger/error).
callout: {
compoundVariants: [
{
color: 'info',
class: {
base: 'border-[#002235] bg-[#00131D] text-[#64C7FF]',
icon: 'text-[#64C7FF]',
},
},
{
color: 'success',
class: {
base: 'border-[#002817] bg-[#00190F] text-[#3CEEA5]',
icon: 'text-[#3CEEA5]',
},
},
{
color: 'warning',
class: {
base: 'border-[#292100] bg-[#1B1500] text-[#FFDC4E]',
icon: 'text-[#FFDC4E]',
},
},
{
color: 'error',
class: {
base: 'border-[#340A01] bg-[#1C0301] text-[#FFA692]',
icon: 'text-[#FFA692]',
},
},
],
},
},
header: {
slots: {
root: 'bg-[rgba(12,13,12,0.8)] backdrop-blur-[20px] backdrop-saturate-200 border-b border-default h-(--ui-header-height) sticky top-0 z-50',
},
},
contentSearchButton: {
slots: {
base: 'bg-[rgba(12,13,12,0.8)] hover:bg-[rgba(18,17,16,0.9)] border border-[#121110]',
},
},
contentSurround: {
slots: {
link: 'bg-[rgba(12,13,12,0.8)] border-[#121110] hover:bg-primary/10 hover:border-primary',
linkLeading: 'bg-[rgba(12,13,12,0.8)] ring-1 ring-[var(--ui-text-highlighted)]/50 group-hover:bg-primary/10 group-hover:ring-primary/50',
},
},
kbd: {
compoundVariants: [
{
color: 'neutral',
variant: 'subtle',
class: 'ring-[#121110] bg-[rgba(12,13,12,0.8)] text-default',
},
],
},
},
})
+37
View File
@@ -0,0 +1,37 @@
/* Restore the old site's near-black dark background (#0B0A0A) instead of
Nuxt UI's default zinc-900 */
.dark {
--ui-bg: #0B0A0A;
/* Same border color used on cards, applied sitewide (header, separators,
the horizontal nav menu row, etc.) for a consistent look */
--ui-border: #121110;
}
/* Old site's container was 1280px with 24px padding each side (1232px of
actual content). Nuxt UI's container uses a bigger lg:px-8 (32px) padding,
so the max-width is bumped to 81rem (1296px) to land on the same 1232px
content width rather than reproducing the outer 1280px figure verbatim. */
:root {
--ui-container: 81rem;
}
/* Docus hardcodes --ui-header-height to 112px (64px header + 48px sub-nav
bar) whenever navigation.sub is 'header', regardless of what's actually
in that bar. Our horizontal menu now lives in the main header row itself
(AppHeaderCenter.vue) and the sub-nav bar (AppHeaderBottom.vue) is empty,
so the header is back to a single 64px row. */
@media (min-width: 1024px) {
.docus-sub-header {
--ui-header-height: 4rem !important;
}
}
/* A screenshot inside a list item (the step-by-step install guides) is
rendered as a bare <img> child of the <li> and gets no margin at all, so it
ends up glued to the text above and below it. The same image in a paragraph
is wrapped in a <p> that carries the prose spacing. Give it that spacing
back so illustrated steps stay readable. */
li > img {
margin-block: 1.25rem;
}
+141
View File
@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 1000 1000">
<!-- Generator: Adobe Illustrator 30.7.0, SVG Export Plug-In . SVG Version: 2.1.4 Build 114) -->
<defs>
<style>
.st0 {
fill: url(#Dégradé_sans_nom_5);
}
.st1 {
fill: url(#Dégradé_sans_nom_264);
stroke: url(#Dégradé_sans_nom_334);
}
.st1, .st2, .st3, .st4, .st5, .st6, .st7, .st8, .st9, .st10, .st11, .st12 {
stroke-miterlimit: 10;
}
.st2 {
fill: url(#Dégradé_sans_nom_262);
stroke: url(#Dégradé_sans_nom_332);
}
.st3 {
fill: url(#Dégradé_sans_nom_269);
stroke: url(#Dégradé_sans_nom_339);
}
.st4 {
fill: url(#Dégradé_sans_nom_263);
stroke: url(#Dégradé_sans_nom_333);
}
.st5 {
fill: url(#Dégradé_sans_nom_265);
stroke: url(#Dégradé_sans_nom_335);
}
.st6 {
fill: url(#Dégradé_sans_nom_268);
stroke: url(#Dégradé_sans_nom_338);
}
.st7 {
fill: url(#Dégradé_sans_nom_261);
stroke: url(#Dégradé_sans_nom_331);
}
.st8 {
fill: url(#Dégradé_sans_nom_266);
stroke: url(#Dégradé_sans_nom_336);
}
.st9 {
fill: url(#Dégradé_sans_nom_267);
stroke: url(#Dégradé_sans_nom_337);
}
.st10 {
fill: url(#Dégradé_sans_nom_26);
stroke: url(#Dégradé_sans_nom_33);
}
.st11 {
fill: url(#Dégradé_sans_nom_2610);
stroke: url(#Dégradé_sans_nom_3310);
}
.st12 {
fill: url(#Dégradé_sans_nom_2611);
stroke: url(#Dégradé_sans_nom_3311);
}
.st13 {
fill: #fff;
}
.st14 {
fill: url(#Dégradé_sans_nom_51);
}
</style>
<linearGradient id="Dégradé_sans_nom_5" data-name="Dégradé sans nom 5" x1="245" y1="503" x2="747" y2="503" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#55c3ec"/>
<stop offset="1" stop-color="#1d71b8" stop-opacity=".8"/>
</linearGradient>
<linearGradient id="Dégradé_sans_nom_51" data-name="Dégradé sans nom 5" x1="399.4" y1="504.3" x2="595.1" y2="504.3" xlink:href="#Dégradé_sans_nom_5"/>
<linearGradient id="Dégradé_sans_nom_26" data-name="Dégradé sans nom 26" x1="74.5" y1="560.2" x2="106.2" y2="591.8" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#55c3ec"/>
<stop offset="1" stop-color="#1d71b8" stop-opacity=".8"/>
</linearGradient>
<linearGradient id="Dégradé_sans_nom_33" data-name="Dégradé sans nom 33" x1="74.2" y1="559.9" x2="106.5" y2="592.2" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#55c3ec"/>
<stop offset="1" stop-color="#1d71b8" stop-opacity=".5"/>
</linearGradient>
<linearGradient id="Dégradé_sans_nom_261" data-name="Dégradé sans nom 26" x1="158.2" y1="648.8" x2="176.7" y2="667.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_331" data-name="Dégradé sans nom 33" x1="157.8" y1="648.4" x2="177.1" y2="667.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_262" data-name="Dégradé sans nom 26" x1="210.2" y1="714.7" x2="249.8" y2="754.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_332" data-name="Dégradé sans nom 33" x1="209.9" y1="714.3" x2="250.2" y2="754.6" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_263" data-name="Dégradé sans nom 26" x1="-54" y1="72.2" x2="-14.4" y2="111.8" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_333" data-name="Dégradé sans nom 33" x1="-54.4" y1="71.9" x2="-14.1" y2="112.2" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_264" data-name="Dégradé sans nom 26" x1="485.1" y1="-9.2" x2="503.7" y2="9.4" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_334" data-name="Dégradé sans nom 33" x1="484.8" y1="-9.6" x2="504" y2="9.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_265" data-name="Dégradé sans nom 26" x1="825.1" y1="682.3" x2="856.8" y2="713.9" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_335" data-name="Dégradé sans nom 33" x1="824.8" y1="681.9" x2="857.1" y2="714.3" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_266" data-name="Dégradé sans nom 26" x1="308.5" y1="356.5" x2="340.3" y2="388.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_336" data-name="Dégradé sans nom 33" x1="308.1" y1="356.2" x2="340.7" y2="388.7" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_267" data-name="Dégradé sans nom 26" x1="540.4" y1="450.4" x2="559" y2="469" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_337" data-name="Dégradé sans nom 33" x1="540.1" y1="450" x2="559.4" y2="469.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_268" data-name="Dégradé sans nom 26" x1="-336.4" y1="326.9" x2="-296.8" y2="366.5" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_338" data-name="Dégradé sans nom 33" x1="-336.7" y1="326.5" x2="-296.4" y2="366.8" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_269" data-name="Dégradé sans nom 26" x1="115" y1="-29.9" x2="133.6" y2="-11.3" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_339" data-name="Dégradé sans nom 33" x1="114.7" y1="-30.2" x2="134" y2="-11" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2610" data-name="Dégradé sans nom 26" x1="94.5" y1="304.2" x2="124.4" y2="334" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_3310" data-name="Dégradé sans nom 33" x1="94.2" y1="303.8" x2="124.7" y2="334.4" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2611" data-name="Dégradé sans nom 26" x1="435.8" y1="254.1" x2="454.4" y2="272.7" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_3311" data-name="Dégradé sans nom 33" x1="435.5" y1="253.8" x2="454.8" y2="273.1" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_33"/>
</defs>
<g id="Calque_1">
<circle class="st13" cx="499.5" cy="499.5" r="499.5"/>
<g>
<path class="st0" d="M618.3,252h-244.6c-72.1,0-128.7,59.2-128.7,128.7v244.6c0,72.1,59.2,128.7,128.7,128.7h244.6c72.1,0,128.7-59.2,128.7-128.7v-244.6c0-72.1-59.2-128.7-128.7-128.7ZM497.3,656.2c-82.4,0-151.9-69.5-151.9-151.9s66.9-151.9,151.9-151.9,151.9,69.5,151.9,151.9-69.5,151.9-151.9,151.9ZM659.5,378.2c-20.6,0-36-15.4-36-36s15.4-36,36-36,36,15.4,36,36c0,20.6-15.4,36-36,36Z"/>
<path class="st14" d="M497.3,406.5c-54.1,0-97.8,43.8-97.8,97.8s43.8,97.8,97.8,97.8,97.8-43.8,97.8-97.8-43.8-97.8-97.8-97.8Z"/>
</g>
</g>
<g id="Calque_2">
<g id="Calque_3">
<circle class="st10" cx="90.4" cy="576" r="22.4"/>
<circle class="st7" cx="175.6" cy="607.9" r="13.1"/>
<circle class="st2" cx="140.8" cy="691.6" r="28"/>
<circle class="st4" cx="829.7" cy="602.6" r="28"/>
<circle class="st1" cx="908.9" cy="562.1" r="13.1"/>
<circle class="st5" cx="840.9" cy="698.1" r="22.4"/>
<circle class="st8" cx="466.1" cy="876.5" r="22.5"/>
<circle class="st9" cx="538.6" cy="839.8" r="13.1"/>
<circle class="st6" cx="686.1" cy="170.1" r="28"/>
<circle class="st3" cx="733.7" cy="247.7" r="13.1"/>
<circle class="st11" cx="236.9" cy="206.5" r="21.1"/>
<circle class="st12" cx="315.4" cy="164.9" r="13.1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 8.8 KiB

+154
View File
@@ -0,0 +1,154 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 1000 1000">
<!-- Generator: Adobe Illustrator 29.7.1, SVG Export Plug-In . SVG Version: 2.1.1 Build 8) -->
<defs>
<style>
.st0 {
fill: url(#Dégradé_sans_nom_265);
stroke: url(#Dégradé_sans_nom_33);
}
.st0, .st1, .st2, .st3, .st4, .st5, .st6, .st7, .st8, .st9, .st10, .st11 {
stroke-miterlimit: 10;
}
.st1 {
fill: url(#Dégradé_sans_nom_269);
stroke: url(#Dégradé_sans_nom_334);
}
.st2 {
fill: url(#Dégradé_sans_nom_268);
stroke: url(#Dégradé_sans_nom_333);
}
.st3 {
fill: url(#Dégradé_sans_nom_266);
stroke: url(#Dégradé_sans_nom_331);
}
.st4 {
fill: url(#Dégradé_sans_nom_267);
stroke: url(#Dégradé_sans_nom_332);
}
.st12 {
fill: url(#Dégradé_sans_nom_261);
}
.st13 {
fill: url(#Dégradé_sans_nom_262);
}
.st14 {
fill: url(#Dégradé_sans_nom_264);
}
.st15 {
fill: url(#Dégradé_sans_nom_263);
}
.st5 {
fill: url(#Dégradé_sans_nom_2616);
stroke: url(#Dégradé_sans_nom_3311);
}
.st6 {
fill: url(#Dégradé_sans_nom_2615);
stroke: url(#Dégradé_sans_nom_3310);
}
.st16 {
fill: #fff;
}
.st17 {
fill: url(#Dégradé_sans_nom_26);
}
.st7 {
fill: url(#Dégradé_sans_nom_2610);
stroke: url(#Dégradé_sans_nom_335);
}
.st8 {
fill: url(#Dégradé_sans_nom_2613);
stroke: url(#Dégradé_sans_nom_338);
}
.st9 {
fill: url(#Dégradé_sans_nom_2614);
stroke: url(#Dégradé_sans_nom_339);
}
.st10 {
fill: url(#Dégradé_sans_nom_2611);
stroke: url(#Dégradé_sans_nom_336);
}
.st11 {
fill: url(#Dégradé_sans_nom_2612);
stroke: url(#Dégradé_sans_nom_337);
}
</style>
<linearGradient id="Dégradé_sans_nom_26" data-name="Dégradé sans nom 26" x1="373.2" y1="159.5" x2="625.1" y2="411.5" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#55c3ec"/>
<stop offset="1" stop-color="#1d71b8" stop-opacity=".8"/>
</linearGradient>
<linearGradient id="Dégradé_sans_nom_261" data-name="Dégradé sans nom 26" x1="143.1" y1="200.5" x2="395" y2="452.4" gradientTransform="translate(30.8 109.3)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_262" data-name="Dégradé sans nom 26" x1="81.3" y1="60.6" x2="333.2" y2="312.5" gradientTransform="translate(187.1 873.6) rotate(-90)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_263" data-name="Dégradé sans nom 26" x1="-44.4" y1="16.5" x2="207.5" y2="268.4" gradientTransform="translate(705.4 808.2) rotate(-180)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_264" data-name="Dégradé sans nom 26" x1="-67.9" y1="-58.9" x2="184" y2="193" gradientTransform="translate(770.9 385.1) rotate(90)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_265" data-name="Dégradé sans nom 26" x1="74.5" y1="560.2" x2="106.2" y2="591.8" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_33" data-name="Dégradé sans nom 33" x1="74.2" y1="559.9" x2="106.5" y2="592.2" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#55c3ec"/>
<stop offset="1" stop-color="#1d71b8" stop-opacity=".5"/>
</linearGradient>
<linearGradient id="Dégradé_sans_nom_266" data-name="Dégradé sans nom 26" x1="158.2" y1="648.8" x2="176.7" y2="667.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_331" data-name="Dégradé sans nom 33" x1="157.8" y1="648.4" x2="177.1" y2="667.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_267" data-name="Dégradé sans nom 26" x1="210.2" y1="714.7" x2="249.8" y2="754.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_332" data-name="Dégradé sans nom 33" x1="209.9" y1="714.3" x2="250.2" y2="754.6" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_268" data-name="Dégradé sans nom 26" x1="-54" y1="72.2" x2="-14.4" y2="111.8" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_333" data-name="Dégradé sans nom 33" x1="-54.4" y1="71.9" x2="-14.1" y2="112.2" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_269" data-name="Dégradé sans nom 26" x1="485.1" y1="-9.2" x2="503.7" y2="9.4" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_334" data-name="Dégradé sans nom 33" x1="484.8" y1="-9.6" x2="504" y2="9.7" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2610" data-name="Dégradé sans nom 26" x1="825.1" y1="682.3" x2="856.8" y2="713.9" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_335" data-name="Dégradé sans nom 33" x1="824.8" y1="681.9" x2="857.1" y2="714.3" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2611" data-name="Dégradé sans nom 26" x1="308.5" y1="356.5" x2="340.3" y2="388.3" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_336" data-name="Dégradé sans nom 33" x1="308.1" y1="356.2" x2="340.7" y2="388.7" gradientTransform="translate(909.8 659.5) rotate(105)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2612" data-name="Dégradé sans nom 26" x1="540.4" y1="450.4" x2="559" y2="469" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_337" data-name="Dégradé sans nom 33" x1="540.1" y1="450" x2="559.4" y2="469.3" gradientTransform="translate(661.8 133.9) rotate(60)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2613" data-name="Dégradé sans nom 26" x1="-336.4" y1="326.9" x2="-296.8" y2="366.5" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_338" data-name="Dégradé sans nom 33" x1="-336.7" y1="326.5" x2="-296.4" y2="366.8" gradientTransform="translate(342.9 490.5) rotate(-175.4)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2614" data-name="Dégradé sans nom 26" x1="115" y1="-29.9" x2="133.6" y2="-11.3" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_339" data-name="Dégradé sans nom 33" x1="114.7" y1="-30.2" x2="134" y2="-11" gradientTransform="translate(814.9 151.4) rotate(139.6)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2615" data-name="Dégradé sans nom 26" x1="94.5" y1="304.2" x2="124.4" y2="334" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_3310" data-name="Dégradé sans nom 33" x1="94.2" y1="303.8" x2="124.7" y2="334.4" gradientTransform="translate(568 142) rotate(97.9)" xlink:href="#Dégradé_sans_nom_33"/>
<linearGradient id="Dégradé_sans_nom_2616" data-name="Dégradé sans nom 26" x1="435.8" y1="254.1" x2="454.4" y2="272.7" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_26"/>
<linearGradient id="Dégradé_sans_nom_3311" data-name="Dégradé sans nom 33" x1="435.5" y1="253.8" x2="454.8" y2="273.1" gradientTransform="translate(257.1 -349) rotate(52.9)" xlink:href="#Dégradé_sans_nom_33"/>
</defs>
<g id="Calque_1">
<circle class="st16" cx="499.5" cy="499.5" r="499.5"/>
</g>
<g id="Calque_2">
<g id="Calque_3">
<ellipse class="st17" cx="499.2" cy="285.5" rx="139.8" ry="209.5"/>
<ellipse class="st12" cx="299.9" cy="435.8" rx="139.8" ry="209.5" transform="translate(-207.3 586.3) rotate(-72)"/>
<ellipse class="st13" cx="373.6" cy="666.3" rx="209.5" ry="139.8" transform="translate(-385.1 576.9) rotate(-54)"/>
<ellipse class="st15" cx="623.9" cy="665.8" rx="139.8" ry="209.5" transform="translate(-272.2 493.9) rotate(-36)"/>
<ellipse class="st14" cx="703.9" cy="443.1" rx="209.5" ry="139.8" transform="translate(-94.9 211.2) rotate(-16)"/>
<circle class="st0" cx="90.4" cy="576" r="22.4"/>
<circle class="st3" cx="175.6" cy="607.9" r="13.1"/>
<circle class="st4" cx="140.8" cy="691.6" r="28"/>
<circle class="st2" cx="829.7" cy="602.6" r="28"/>
<circle class="st1" cx="908.9" cy="562.1" r="13.1"/>
<circle class="st7" cx="840.9" cy="698.1" r="22.4"/>
<circle class="st10" cx="466.1" cy="876.5" r="22.5"/>
<circle class="st11" cx="538.6" cy="839.8" r="13.1"/>
<circle class="st8" cx="686.1" cy="170.1" r="28"/>
<circle class="st9" cx="733.7" cy="247.7" r="13.1"/>
<circle class="st6" cx="236.9" cy="206.5" r="21.1"/>
<circle class="st5" cx="315.4" cy="164.9" r="13.1"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 9.6 KiB

+84
View File
@@ -0,0 +1,84 @@
<script lang="ts" setup>
const { title, description, headline } = defineProps<{ title?: string, description?: string, headline?: string }>()
const appConfig = useAppConfig()
const { name: siteName } = useSiteConfig()
const primaryColor = appConfig.ui?.colors?.primary ?? 'emerald'
const logoPath = appConfig.header?.logo?.dark || appConfig.header?.logo?.light
const logoHeight = 40
const logoSvg = await fetchLogoSvg(logoPath)
async function fetchLogoSvg(path?: string): Promise<string> {
if (!path) return ''
try {
const { url: siteUrl } = useSiteConfig()
const url = path.startsWith('http') ? path : `${siteUrl}${path}`
let svg = await $fetch<string>(url, { responseType: 'text' })
// Strip the XML prolog and comments: takumi renders them as literal text
// instead of ignoring them like a browser's innerHTML would.
svg = svg.replace(/<\?xml[^>]*\?>/, '').replace(/<!--[\s\S]*?-->/g, '').trim()
// takumi doesn't resolve the SVG's own <style> class rules either (paths
// rendered black), so inline each class's fill directly, then drop <defs>.
const classFills = new Map(
[...svg.matchAll(/\.(\w+)\s*\{\s*fill:\s*([^;}\s]+)/g)].map(([, className, fill]) => [className, fill]),
)
for (const [className, fill] of classFills) {
svg = svg.replaceAll(`class="${className}"`, `fill="${fill}"`)
}
svg = svg.replace(/<defs>[\s\S]*?<\/defs>/, '').trim()
// This logo is a wide wordmark (viewBox ~3360x576), not a square icon,
// so width must scale from its own aspect ratio instead of a fixed value.
const viewBox = svg.match(/viewBox="[\d.]+ [\d.]+ ([\d.]+) ([\d.]+)"/)
const width = viewBox ? Math.round(logoHeight * (Number(viewBox[1]) / Number(viewBox[2]))) : logoHeight
return svg.replace('<svg', `<svg width="${width}" height="${logoHeight}"`)
}
catch {
return ''
}
}
</script>
<template>
<div class="w-full h-full flex flex-col justify-between bg-[#0B0A0A] px-[80px] py-[60px] font-[Roboto]">
<!-- Same shape, colors and blur as the site's own :ellipsis component: a wide
flat oval filled with its diagonal blue/cyan gradient, then blurred. -->
<div class="absolute blur-3xl top-[80px] right-[50px] w-[900px] h-[360px] rounded-full bg-[linear-gradient(97.62deg,rgba(0,71,225,0.18)_2.27%,rgba(26,214,255,0.12)_65%,rgba(0,71,225,0.12)_98.48%)]" />
<div class="flex-1 flex flex-col justify-center">
<p
v-if="headline"
:class="`uppercase text-[22px] font-bold m-0 mb-5 tracking-[0.05em] text-${primaryColor}-500`"
>
{{ headline }}
</p>
<h1
v-if="title"
class="m-0 mb-6 text-[50px] font-bold text-white leading-[1.1] w-full max-w-[900px] wrap-break-word"
>
{{ title?.slice(0, 60) }}
</h1>
<p
v-if="description"
class="m-0 text-[28px] text-neutral-400 leading-[1.4] w-full max-w-[900px] wrap-break-word"
>
{{ description?.slice(0, 200) }}
</p>
</div>
<div class="flex">
<div
v-if="logoSvg"
class="h-[40px]"
v-html="logoSvg"
/>
<div v-else class="text-white text-[18px] font-normal rounded-lg px-5 py-2">
{{ siteName }}
</div>
</div>
</div>
</template>
+102
View File
@@ -0,0 +1,102 @@
<script setup lang="ts">
const appConfig = useAppConfig()
const { forced: forcedColorMode } = useDocusColorMode()
const { isEnabled: isAssistantEnabled } = useAssistant()
const { isEnabled, locales } = useDocusI18n()
const { subNavigationMode } = useSubNavigation()
const links = computed(() => {
const list = []
if (appConfig.socials?.gitea) {
list.push({
'icon': 'i-simple-icons-gitea',
'to': appConfig.socials.gitea,
'target': '_blank',
'aria-label': 'Gitea',
})
}
if (appConfig.github?.url) {
list.push({
'icon': 'i-simple-icons-github',
'to': appConfig.github.url,
'target': '_blank',
'aria-label': 'GitHub',
})
}
return list
})
</script>
<template>
<UHeader
:ui="{ center: 'flex-1' }"
:class="{ 'flex flex-col': subNavigationMode === 'header' }"
>
<AppHeaderCenter />
<template #left>
<AppHeaderLeft />
</template>
<template #right>
<AppHeaderCTA />
<template v-if="isAssistantEnabled">
<AssistantChat />
</template>
<template v-if="isEnabled && locales.length > 1">
<ClientOnly>
<LanguageSelect />
<template #fallback>
<div class="h-8 w-8 animate-pulse bg-neutral-200 dark:bg-neutral-800 rounded-md" />
</template>
</ClientOnly>
<USeparator
orientation="vertical"
class="h-8"
/>
</template>
<UContentSearchButton class="lg:hidden" />
<ClientOnly v-if="!forcedColorMode">
<UColorModeButton />
<template #fallback>
<div class="h-8 w-8 animate-pulse bg-neutral-200 dark:bg-neutral-800 rounded-md" />
</template>
</ClientOnly>
<template v-if="links?.length">
<UButton
v-for="(link, index) of links"
:key="index"
v-bind="{ color: 'neutral', variant: 'ghost', ...link }"
/>
</template>
</template>
<template #toggle="{ open, toggle }">
<IconMenuToggle
:open="open"
class="lg:hidden"
@click="toggle"
/>
</template>
<template #body>
<AppHeaderBody />
</template>
<template
v-if="subNavigationMode === 'header'"
#bottom
>
<AppHeaderBottom />
</template>
</UHeader>
</template>
+3
View File
@@ -0,0 +1,3 @@
<template>
<div />
</template>
+47
View File
@@ -0,0 +1,47 @@
<script setup lang="ts">
const { sections } = useSubNavigation()
const navMenuVariants = useUIConfig('navigationMenu')
</script>
<template>
<template v-if="sections.length">
<!-- Empty spacer: keeps the header's flex-1 center slot from collapsing
while the real menu below is absolutely positioned so it can match
the article's content-column width instead of this slot's width. -->
<div class="hidden lg:block w-full" />
<UContainer class="absolute inset-x-0 inset-y-0 hidden lg:flex items-center pointer-events-none">
<!-- Mirrors the docs page's actual layout: an outer 10-col grid (left
doc-tree sidebar = col-span-2) containing a second, nested 10-col
grid for the article body (right TOC sidebar = col-span-2 of that
inner grid). Matching both levels is what lines this menu up with
the real content column instead of a naive single-level fraction. -->
<div class="grid grid-cols-10 gap-10 w-full">
<div class="col-span-8 col-start-3 grid grid-cols-10 gap-10">
<div class="col-span-8 col-start-1 pointer-events-auto">
<UNavigationMenu
:items="sections"
:highlight="navMenuVariants.highlight ?? true"
:highlight-color="navMenuVariants.highlightColor"
:variant="navMenuVariants.variant ?? 'pill'"
:color="navMenuVariants.color"
class="-mx-[10px] w-[calc(100%+20px)] [&>div]:w-full"
:ui="{ list: 'w-full justify-between', item: 'py-0' }"
/>
</div>
</div>
</div>
</UContainer>
</template>
<UContentSearchButton
v-else
:collapsed="false"
class="w-full"
variant="soft"
:ui="{
leadingIcon: 'size-4 mx-0.5',
}"
/>
</template>
+43
View File
@@ -0,0 +1,43 @@
<script setup lang="ts">
withDefaults(defineProps<{
width?: string
height?: string
zIndex?: string
top?: string
left?: string
right?: string
blur?: string
colors?: string[]
}>(), {
width: '10rem',
height: '10rem',
zIndex: '10',
top: '0',
left: 'auto',
right: 'auto',
blur: '50px',
colors: () => ['rgba(0, 71, 225, 0.34)', 'rgba(26, 214, 255, 0.22)', 'rgba(0, 71, 225, 0.22)'],
})
</script>
<template>
<div
class="pointer-events-none absolute w-full"
:style="{
top,
insetInlineStart: left,
insetInlineEnd: right,
zIndex,
maxWidth: width,
height,
filter: `blur(${blur})`,
}"
>
<div
class="w-full h-full"
:style="{
background: `linear-gradient(97.62deg, ${colors[0]} 2.27%, ${colors[1]} 65%, ${colors[2]} 98.48%)`,
}"
/>
</div>
</template>
+37
View File
@@ -0,0 +1,37 @@
<script setup lang="ts">
export type FileTreeEntry = string | Record<string, FileTreeEntry[]>
const props = withDefaults(defineProps<{
tree: FileTreeEntry
label?: string
collapsed?: boolean
}>(), {
label: 'Folder structure',
collapsed: false,
})
// `collapsed` only sets the initial state; the header click below then
// toggles this independently of the prop.
const isOpen = ref(!props.collapsed)
</script>
<template>
<div class="not-prose my-5 rounded-lg overflow-hidden bg-elevated/50 ring ring-default divide-y divide-default">
<button
type="button"
class="flex items-center gap-1.5 w-full px-4 py-3 text-muted hover:text-default hover:bg-elevated/50 transition-colors cursor-pointer"
@click="isOpen = !isOpen"
>
<UIcon name="i-lucide-folder-tree" class="size-4 shrink-0" />
<span class="text-sm/6">{{ label }}</span>
<UIcon
name="i-lucide-chevron-down"
class="size-4 shrink-0 ms-auto transition-transform"
:class="isOpen ? '' : '-rotate-90'"
/>
</button>
<ul v-show="isOpen" class="text-sm leading-relaxed px-2 py-2 list-none">
<FileTreeNode :entry="tree" root />
</ul>
</div>
</template>
+138
View File
@@ -0,0 +1,138 @@
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'
import codeIconTheme from '#build/ui/prose/code-icon'
import type { FileTreeEntry } from './FileTree.vue'
const props = withDefaults(defineProps<{
entry: FileTreeEntry
root?: boolean
parentPath?: string
isLast?: boolean
}>(), {
root: false,
parentPath: '',
isLast: false,
})
function splitComment(raw: string) {
const index = raw.indexOf(' #')
if (index === -1) return { text: raw, comment: undefined as string | undefined }
return { text: raw.slice(0, index).trimEnd(), comment: raw.slice(index + 2).trim() }
}
const rawEntry = computed(() => typeof props.entry === 'object' ? Object.keys(props.entry)[0] : props.entry as string)
const parsed = computed(() => splitComment(rawEntry.value))
const isFolder = computed(() => typeof props.entry === 'object' || parsed.value.text.endsWith('/'))
// Strip a trailing "/" marker, except when it's the whole name: that's the
// filesystem root itself, written as a bare "/".
const name = computed(() => {
const text = parsed.value.text
return text.length > 1 && text.endsWith('/') ? text.slice(0, -1) : text
})
const comment = computed(() => parsed.value.comment)
const children = computed<FileTreeEntry[]>(() => {
if (typeof props.entry !== 'object') return []
return Object.values(props.entry)[0] || []
})
// The root's own name is "/" already; every other node just appends its
// name to its parent's path, without doubling that leading slash.
const fullPath = computed(() => {
if (props.root) return name.value
return props.parentPath === '/' ? `/${name.value}` : `${props.parentPath}/${name.value}`
})
const { copy, copied } = useClipboard({ source: fullPath })
function onClick() {
copy()
}
const appConfig = useAppConfig()
// Same lookup order as Nuxt UI's own CodeIcon.vue (exact filename match,
// then extension, then the vscode-icons fallback), so a file gets the same
// icon here as it would in a labeled code fence.
const icon = computed(() => {
if (isFolder.value) return 'i-lucide-folder'
const filename = name.value
const icons = { ...codeIconTheme, ...(appConfig.ui?.prose?.codeIcon || {}) } as Record<string, string>
const extension = filename.includes('.') ? filename.split('.').pop() : undefined
return icons[filename.toLowerCase()]
?? (extension && icons[extension])
?? (extension && `i-vscode-icons-file-type-${extension}`)
?? 'i-lucide-file'
})
</script>
<template>
<li class="relative" :class="root ? '' : 'ps-3'">
<!-- Not the last child: a plain full-height guide line, since it needs
to keep going for the next sibling below it anyway. This is a direct
child of the LI (not the row span below, like the other guides),
so its own "start-0" lines up with the row span's "-start-1.5":
the row span sits 1.5 further in (its "-mx-1.5), so its own offset
needs those same 1.5 taken back out to land on the same column. -->
<span v-if="!root && !isLast" class="absolute start-0 top-0 bottom-0 w-px bg-white/20" />
<span
class="group flex items-center gap-1.5 py-1 px-1.5 -mx-1.5 rounded-md relative hover:bg-elevated/50 transition-colors cursor-pointer"
title="Copy path"
@click="onClick"
>
<!-- Last child: one rounded corner (border-inline-start + border-block-end
on a single box) instead of a separate vertical + horizontal stroke,
so the join is one clean curve rather than two translucent strokes
stacking into a visibly brighter square where they cross. Sized off
this row's own box (top to its vertical center) instead of a guessed
pixel height, so it stays in sync if the row's height ever changes. -->
<span
v-if="!root && isLast"
class="absolute -start-1.5 top-0 bottom-1/2 w-3 border-s border-b border-white/20 rounded-es-md"
/>
<!-- Not the last child: just the branch into the icon — the vertical
guide itself is the LI-level line above, offset a hair to the
right of it so the two strokes sit side by side instead of
overlapping. -->
<span
v-if="!root && !isLast"
class="absolute -start-[5px] top-1/2 -translate-y-1/2 w-3 h-px bg-white/20"
/>
<!-- Bridges the gap between this icon's own bottom edge and where its
children's guide lines start (right at this row's bottom edge,
which is exactly where the child <ul> begins), so the line reads
as coming out of the folder icon rather than piercing through it
or starting in mid-air. Starts at the row's center plus half the
icon's own height (size-4 = 16px), so it clears the icon glyph
regardless of the row's actual height. -->
<span
v-if="isFolder && children.length"
class="absolute start-3.5 top-[calc(50%+8px)] bottom-0 w-px bg-white/20"
/>
<UIcon
:name="icon"
class="shrink-0 size-4"
:class="isFolder ? 'text-[var(--ui-primary)]' : 'text-[var(--ui-text-dimmed)]'"
/>
<span>{{ name }}</span>
<span v-if="comment" class="text-xs text-muted italic">{{ comment }}</span>
<UIcon
:name="copied ? 'i-lucide-check' : 'i-lucide-copy'"
class="size-3.5 shrink-0 opacity-0 group-hover:opacity-100 transition-opacity text-muted"
/>
</span>
<ul v-if="children.length" class="ms-2 ps-0 list-none">
<FileTreeNode
v-for="(child, i) in children"
:key="i"
:entry="child"
:parent-path="fullPath"
:is-last="i === children.length - 1"
/>
</ul>
</li>
</template>
+17
View File
@@ -0,0 +1,17 @@
<script setup lang="ts">
const { sidebarNavigation } = useSubNavigation()
const contentNavVariants = useUIConfig('contentNavigation')
</script>
<template>
<UContentNavigation
:collapsible="true"
:default-open="false"
:highlight="contentNavVariants.highlight ?? true"
:highlight-color="contentNavVariants.highlightColor"
:variant="contentNavVariants.variant ?? 'link'"
:color="contentNavVariants.color"
:navigation="sidebarNavigation"
/>
</template>
+30
View File
@@ -0,0 +1,30 @@
<script setup lang="ts">
const { subNavigationMode, sections } = useSubNavigation()
</script>
<template>
<div
v-if="subNavigationMode === 'aside'"
class="mb-2"
>
<UPageAnchors :links="sections" />
<USeparator
type="dashed"
class="my-4"
/>
</div>
<div
v-else
class="mb-4"
>
<UContentSearchButton
:collapsed="false"
class="w-full"
variant="soft"
:ui="{
leadingIcon: 'size-4 mx-0.5',
}"
/>
</div>
</template>
+102
View File
@@ -0,0 +1,102 @@
<script setup lang="ts">
import { useClipboard } from '@vueuse/core'
import { joinURL, withTrailingSlash } from 'ufo'
import { useRuntimeConfig } from '#imports'
const route = useRoute()
const toast = useToast()
const runtimeConfig = useRuntimeConfig()
const appBaseURL = runtimeConfig.app?.baseURL || '/'
const mcpRoute = (runtimeConfig.public.mcp as { route?: string } | undefined)?.route || '/mcp'
const { copy, copied } = useClipboard()
const { t } = useDocusI18n()
const markdownLink = computed(() => `${window?.location?.origin}${withTrailingSlash(appBaseURL)}raw${route.path}.md`)
const mcpServerUrl = computed(() => `${window?.location?.origin}${joinURL(appBaseURL, mcpRoute)}`)
const mcpDeeplink = computed(() => `${window?.location?.origin}${joinURL(appBaseURL, mcpRoute, 'deeplink')}`)
const items = computed(() => [
[{
label: t('docs.copy.link'),
icon: 'i-lucide-link',
onSelect() {
copy(markdownLink.value)
},
},
{
label: t('docs.copy.view'),
icon: 'i-simple-icons:markdown',
target: '_blank',
to: markdownLink.value,
},
{
label: t('docs.copy.gpt'),
icon: 'i-simple-icons:openai',
target: '_blank',
to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`,
},
{
label: t('docs.copy.claude'),
icon: 'i-simple-icons:anthropic',
target: '_blank',
to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`,
}],
[
{
label: 'Copy MCP Server URL',
icon: 'i-lucide-link',
onSelect() {
copy(mcpServerUrl.value)
toast.add({
title: 'Copied to clipboard',
icon: 'i-lucide-check-circle',
})
},
},
{
label: 'Add MCP Server',
icon: 'i-simple-icons:cursor',
target: '_blank',
to: mcpDeeplink.value,
},
],
])
async function copyPage() {
const page = await $fetch<string>(`/raw${route.path}.md`)
copy(page)
}
</script>
<template>
<UFieldGroup size="sm">
<UButton
:label="t('docs.copy.page')"
:icon="copied ? 'i-lucide-check' : 'i-lucide-copy'"
color="neutral"
variant="soft"
class="bg-[rgba(12,13,12,0.8)] hover:bg-[rgba(18,17,16,0.9)] border border-[#121110]"
:ui="{
leadingIcon: 'text-neutral size-3.5',
}"
@click="copyPage"
/>
<UDropdownMenu
size="sm"
:items="items"
:content="{
align: 'end',
side: 'bottom',
sideOffset: 8,
}"
>
<UButton
icon="i-lucide-chevron-down"
color="neutral"
variant="soft"
class="bg-[rgba(12,13,12,0.8)] hover:bg-[rgba(18,17,16,0.9)] border border-[#121110] border-l-[#121110]"
/>
</UDropdownMenu>
</UFieldGroup>
</template>
+22
View File
@@ -0,0 +1,22 @@
<script setup lang="ts">
import { useAppConfig } from '#imports'
import Callout from '#ui/components/prose/Callout.vue'
// Set `::caution{icon=""}` in the markdown to hide the default icon for
// that one instance (useful when the body already starts with its own
// emoji).
const props = withDefaults(defineProps<{ icon?: string }>(), {
icon: undefined,
})
const appConfig = useAppConfig()
const icon = computed(() => props.icon !== undefined ? props.icon : appConfig.ui.icons.caution)
</script>
<template>
<Callout
color="error"
:icon="icon"
>
<slot mdc-unwrap="p" />
</Callout>
</template>
+21
View File
@@ -0,0 +1,21 @@
<script setup lang="ts">
import { useAppConfig } from '#imports'
import Callout from '#ui/components/prose/Callout.vue'
// Set `::note{icon=""}` in the markdown to hide the default icon for that
// one instance (useful when the body already starts with its own emoji).
const props = withDefaults(defineProps<{ icon?: string }>(), {
icon: undefined,
})
const appConfig = useAppConfig()
const icon = computed(() => props.icon !== undefined ? props.icon : appConfig.ui.icons.info)
</script>
<template>
<Callout
color="info"
:icon="icon"
>
<slot mdc-unwrap="p" />
</Callout>
</template>
+21
View File
@@ -0,0 +1,21 @@
<script setup lang="ts">
import { useAppConfig } from '#imports'
import Callout from '#ui/components/prose/Callout.vue'
// Set `::tip{icon=""}` in the markdown to hide the default icon for that
// one instance (useful when the body already starts with its own emoji).
const props = withDefaults(defineProps<{ icon?: string }>(), {
icon: undefined,
})
const appConfig = useAppConfig()
const icon = computed(() => props.icon !== undefined ? props.icon : appConfig.ui.icons.tip)
</script>
<template>
<Callout
color="success"
:icon="icon"
>
<slot mdc-unwrap="p" />
</Callout>
</template>
+22
View File
@@ -0,0 +1,22 @@
<script setup lang="ts">
import { useAppConfig } from '#imports'
import Callout from '#ui/components/prose/Callout.vue'
// Set `::warning{icon=""}` in the markdown to hide the default icon for
// that one instance (useful when the body already starts with its own
// emoji).
const props = withDefaults(defineProps<{ icon?: string }>(), {
icon: undefined,
})
const appConfig = useAppConfig()
const icon = computed(() => props.icon !== undefined ? props.icon : appConfig.ui.icons.warning)
</script>
<template>
<Callout
color="warning"
:icon="icon"
>
<slot mdc-unwrap="p" />
</Callout>
</template>
+200
View File
@@ -0,0 +1,200 @@
<script setup lang="ts">
import { kebabCase } from 'scule'
import type { ContentNavigationItem, Collections, DocsCollectionItem } from '@nuxt/content'
import { findPageHeadline } from '@nuxt/content/utils'
definePageMeta({
layout: 'docs',
})
const route = useRoute()
const { locale, isEnabled, t } = useDocusI18n()
const { isOpen } = useAssistant()
const appConfig = useAppConfig()
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
const collectionName = computed(() => isEnabled.value ? `docs_${locale.value}` : 'docs')
const [{ data: page }, { data: surround }] = await Promise.all([
useAsyncData(kebabCase(route.path), () => queryCollection(collectionName.value as keyof Collections).path(route.path).first() as Promise<DocsCollectionItem>),
useAsyncData(`${kebabCase(route.path)}-surround`, () => {
return queryCollectionItemSurroundings(collectionName.value as keyof Collections, route.path, {
fields: ['description'],
})
}),
])
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
}
const title = page.value.seo?.title || page.value.title
const description = page.value.seo?.description || page.value.description
const headline = ref(findPageHeadline(navigation?.value, page.value?.path))
const breadcrumbs = computed(() => findPageBreadcrumbs(navigation?.value, page.value?.path || ''))
// Set `hideHeader: true` in a page's frontmatter to skip the title/description
// block entirely (e.g. for a page that builds its own custom layout).
const hideHeader = computed(() => !!(page.value as unknown as Record<string, unknown>)?.hideHeader)
// Set `hideCopyPage: true` in a page's frontmatter to hide the "Copy page"
// dropdown (copy link / view as markdown / open in ChatGPT / Claude).
const hideCopyPage = computed(() => !!(page.value as unknown as Record<string, unknown>)?.hideCopyPage)
// Set `hideToc: true` in a page's frontmatter to hide the right-hand
// "On this page" table-of-contents sidebar.
const hideToc = computed(() => !!(page.value as unknown as Record<string, unknown>)?.hideToc)
useSeo({
title,
description,
type: 'article',
modifiedAt: (page.value as unknown as Record<string, unknown>).modifiedAt as string | undefined,
breadcrumbs,
})
watch(() => navigation?.value, () => {
headline.value = findPageHeadline(navigation?.value, page.value?.path) || headline.value
})
defineOgImage('Docs', {
headline: headline.value,
title: title?.slice(0, 60),
description: formatOgDescription(title, description),
})
const github = computed(() => appConfig.github ? appConfig.github : null)
const giteaUrl = computed(() => appConfig.socials?.gitea as string | undefined)
// "Edit this page" points at Gitea (git.djeex.fr), not the GitHub mirror.
// Gitea's edit route is `/{owner}/{repo}/_edit/{branch}/{path}` (note the
// leading underscore — different from GitHub's `/edit/{branch}/{path}`).
const editLink = computed(() => {
if (!giteaUrl.value) {
return
}
return [
giteaUrl.value,
'_edit',
'main',
'content',
`${page.value?.stem}.${page.value?.extension}`,
].filter(Boolean).join('/')
})
const contributors = computed(() => (page.value as unknown as Record<string, unknown>)?.contributors as string[] | undefined)
const historyLink = computed(() => {
if (!giteaUrl.value) {
return
}
return [
giteaUrl.value,
'commits',
'branch',
'main',
'content',
`${page.value?.stem}.${page.value?.extension}`,
].filter(Boolean).join('/')
})
// Add the page path to the prerender list
addPrerenderPath(`/raw${route.path}.md`)
</script>
<template>
<UPage
v-if="page"
class="relative"
:ui="isOpen ? { center: 'lg:col-span-10' } : undefined"
>
<UPageHeader
v-if="!hideHeader"
:title="page.title"
:description="page.description"
:headline="headline"
:ui="{
wrapper: 'flex-row items-center flex-wrap justify-between',
}"
>
<template #links>
<UButton
v-for="(link, index) in (page as DocsCollectionItem).links"
:key="index"
size="sm"
v-bind="link"
/>
<DocsPageHeaderLinks v-if="!hideCopyPage" />
</template>
</UPageHeader>
<UPageBody>
<ContentRenderer
v-if="page"
:value="page"
/>
<USeparator v-if="giteaUrl || github">
<div
class="flex items-center gap-2 text-sm text-muted"
>
<UButton
v-if="editLink"
variant="link"
color="neutral"
:to="editLink"
target="_blank"
icon="i-lucide-pen"
:ui="{ leadingIcon: 'size-4' }"
>
{{ t('docs.edit') }}
</UButton>
<template v-if="giteaUrl">
<span>{{ t('common.or') }}</span>
<UButton
variant="link"
color="neutral"
:to="`${giteaUrl}/issues/new`"
target="_blank"
icon="i-lucide-alert-circle"
:ui="{ leadingIcon: 'size-4' }"
>
{{ t('docs.report') }}
</UButton>
</template>
</div>
</USeparator>
<div
v-if="contributors?.length"
class="flex items-center gap-2 text-sm text-muted"
>
<UIcon
name="i-lucide-users"
class="size-4 shrink-0"
/>
<span>{{ locale === 'fr' ? (contributors.length > 1 ? 'Contributeurs' : 'Contributeur') : (contributors.length > 1 ? 'Contributors' : 'Contributor') }}:</span>
<ULink
v-if="historyLink"
:to="historyLink"
target="_blank"
class="text-highlighted hover:underline"
>
{{ contributors.join(', ') }}
</ULink>
<span v-else>{{ contributors.join(', ') }}</span>
</div>
<UContentSurround :surround="surround" />
</UPageBody>
<template
v-if="!isOpen && !hideToc"
#right
>
<DocsAsideRight
:page="page"
/>
</template>
</UPage>
</template>
-87
View File
@@ -1,87 +0,0 @@
@media (min-width: 1024px) {
.card-grid .layout {
grid-template-columns: repeat(2, minmax(0, 1fr)) !important;
}
}
.alert .shiki {
--shiki-dark: #00000000 !important;
--shiki-default: #00000000 !important;
--shiki-dark-bg: #00000000 !important;
--shiki-default-bg: #00000000 !important;
}
.dark .shiki {
background-color: #00000000 !important;
}
*html .dark .shiki span, html.dark .shiki span {
background-color: var(--prose-code-block-backgroundColor) !important;
}
/*html .dark .shiki span, html.dark .shiki span {
background-color: #00000000 !important;
}*/
.alert.success .prose-code, .alert.success .shiki span {
background-color: var(--elements-state-success-backgroundColor-secondary) !important;
border-color: #00361f !important;
}
.alert.info .prose-code, .alert.info .shiki span {
background-color: var(--elements-state-info-backgroundColor-secondary) !important;
border-color: #00304a !important;
}
.alert.warning .prose-code, .alert.warning .shiki span {
background-color: var(--elements-state-warning-backgroundColor-secondary) !important;
border-color: #382d00 !important;
}
.alert.danger .prose-code, .alert.danger .shiki span {
background-color: var(--elements-danger-info-backgroundColor-secondary) !important;
border-color: #00304a !important;
}
.section.right > :nth-child(2) {
display:none;
}
.container {
max-width: var(--elements-container-maxWidth);
}
.has-parent-icon .icon {
color: #ADA9A4;
}
.has-parent-icon.active .icon {
color: var(--color-primary-500) !important;
}
.card:hover{
color:#00304a;
}
p img {
border-radius:7px;
}
@media (min-width: 1024px) {
.card-grid {
padding-bottom: 80px !important;
}
}
.prose-code.highlight-sh code .line {
padding-inline-start: 0 !important;
}
.prose-code.highlight-sh code .line:before {
display:none !important;
}
.prose-code.highlight-bash code .line {
padding-inline-start: 0 !important;
}
.prose-code.highlight-bash code .line:before {
display:none !important;
}
-3
View File
@@ -1,3 +0,0 @@
<template>
<img width="120" src="/img/logo.svg"/>
</template>
+96
View File
@@ -0,0 +1,96 @@
import type { DefinedCollection } from '@nuxt/content'
import { defineContentConfig, defineCollection, z } from '@nuxt/content'
import { useNuxt } from '@nuxt/kit'
import { joinURL } from 'ufo'
import { existsSync } from 'node:fs'
const { options } = useNuxt()
const cwd = joinURL(options.rootDir, 'content')
const locales = options.i18n?.locales
// Same checks as docus's own content.config.ts (node_modules/docus/utils/pages.ts,
// not a published package export, so reimplemented here rather than imported).
function landingPageExists(rootDir: string): boolean {
return existsSync(joinURL(rootDir, 'app', 'pages', 'index.vue'))
}
function docsFolderExists(rootDir: string, locale?: string): boolean {
return existsSync(locale ? joinURL(rootDir, 'content', locale, 'docs') : joinURL(rootDir, 'content', 'docs'))
}
const hasLandingPage = landingPageExists(options.rootDir)
const hasDocsFolder = docsFolderExists(options.rootDir)
// Same as docus's own createDocsSchema(), plus the two custom per-page
// frontmatter toggles used by app/pages/[[lang]]/[...slug].vue. Nuxt
// Content's Zod schema silently strips any frontmatter key that isn't
// declared here, which is why hideHeader/hideCopyPage did nothing until
// this schema was extended.
const createDocsSchema = () => z.object({
links: z.array(z.object({
label: z.string(),
icon: z.string(),
to: z.string(),
target: z.string().optional(),
})).optional(),
hideHeader: z.boolean().optional(),
hideCopyPage: z.boolean().optional(),
hideToc: z.boolean().optional(),
contributors: z.array(z.string()).optional(),
})
let collections: Record<string, DefinedCollection>
if (locales && Array.isArray(locales)) {
collections = {}
for (const locale of locales) {
const code = (typeof locale === 'string' ? locale : locale.code).replace('-', '_')
const hasLocaleDocs = docsFolderExists(options.rootDir, code)
if (!hasLandingPage) {
collections[`landing_${code}`] = defineCollection({
type: 'page',
source: {
cwd,
include: `${code}/index.md`,
},
})
}
collections[`docs_${code}`] = defineCollection({
type: 'page',
source: {
cwd,
include: hasLocaleDocs ? `${code}/docs/**` : `${code}/**/*`,
prefix: hasLocaleDocs ? `/${code}/docs` : `/${code}`,
exclude: [`${code}/index.md`],
},
schema: createDocsSchema(),
})
}
}
else {
collections = {
docs: defineCollection({
type: 'page',
source: {
cwd,
include: hasDocsFolder ? 'docs/**' : '**',
prefix: hasDocsFolder ? '/docs' : '/',
exclude: ['index.md'],
},
schema: createDocsSchema(),
}),
}
if (!hasLandingPage) {
collections.landing = defineCollection({
type: 'page',
source: {
cwd,
include: 'index.md',
},
})
}
}
export default defineContentConfig({ collections })
-36
View File
@@ -1,36 +0,0 @@
---
title: Home
description: Homelab documentation by Djeex — self-hosting guides for Debian, Docker, networking, storage, and more.
navigation: false
layout: page
main:
fluid: false
---
:ellipsis{right=0px width=75% blur=150px}
::block-hero
---
cta:
- Access the Docs
- /about/welcome
secondary:
- 🇫🇷 →
- https://docu.djeex.fr/fr/
---
#title
Welcome to docu[·]{style="color: #1ad6ff"}djeex
#description
Docs, more docs. Tips and experiments. Build your homelab and your own NAS.
#extra
![](/img/global/docudjeex-home.svg)
#support
::card{icon=cib:gitea style="color:#1ad6ff;"}
#title
__git.djeex.fr__
#description
[Check my nonsense projects](https://git.djeex.fr)
::
-45
View File
@@ -1,45 +0,0 @@
---
icon: lucide:home
title: Welcome
description: Introduction to Docudjeex — a personal homelab documentation site covering self-hosted services, Debian, and Docker infrastructure.
main:
fluid: false
---
:ellipsis{right=0px width=75% blur=150px}
# docu[·]{style="color: #1ad6ff"}what?
__Docu[·]{style="color: #1ad6ff"}djeex__ is a site containing the documentation of my personal servers, originally created to easily keep track of my configurations and commands.
My infrastructure is built around the Debian 13 + Docker combo, making exporting and deployment simpler.
Special thanks to __Nipah__, __Xenio__, and others for their patience and support. Most of this content comes directly from them.
## About the documentation
The documentation provided here is experimental and shared in a spirit of open knowledge and experience.
It is not intended to build production-grade or industrialized infrastructure.
It may contain mistakes and/or approximations.
Naturally, this documentation should only be used within a strictly legal framework.
::card-grid
#title
Available or Upcoming Documentation
#root
:ellipsis{left=0px width=40rem top=10rem blur=140px}
#default
::card{icon=noto:microscope}
#title
Serveex
#description
[Step-by-step Homelab Deployment Guide](/serveex/introduction)
::
::card{icon=noto:computer-disk}
#title
Stockeex
#description
*(coming soon)* Build your own home NAS to store your data and media
::
::
-3
View File
@@ -1,3 +0,0 @@
icon: noto:star
navigation.title: About
navigation.redirect: /about/welcome
-2
View File
@@ -1,2 +0,0 @@
navigation.title: Networking
icon: lucide:network
-2
View File
@@ -1,2 +0,0 @@
navigation.title: Storage
icon: lucide:hard-drive
-2
View File
@@ -1,2 +0,0 @@
navigation.title: Hardware
icon: lucide:server
-3
View File
@@ -1,3 +0,0 @@
icon: noto:open-book
navigation.title: General
navigation.redirect: /general/networking/nat
-279
View File
@@ -1,279 +0,0 @@
---
icon: lucide:bookmark
navigation: true
title: Introduction
description: Introduction to Serveex — a personal homelab project to self-host everyday services using Debian and Docker, replacing Google, Apple, and Netflix.
main:
fluid: false
---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
## A Home Lab by a Beginner, for Beginners
![](/img/serveex/serveex-server.svg)
**Serveex** is primarily a personal project aimed at hosting as many everyday services as possible at home, without relying on proprietary platforms (Google, Apple, Netflix, etc.). The goal was to experiment, learn, and document every step along the way. This is purely a scientific project and is not intended for production use.
A big thanks to **Nipah** for sharing his infinite knowledge and, above all, for his patience.
::alert{type="info"}
**Prerequisites:**
:::list{type="primary"}
- Have [an online VPS](https://www.it-connect.fr/les-serveurs-prives-virtuels-vps-pour-les-debutants/) or a local machine: ideally a mini PC (you can find N100 models for around €100), but it also works on a laptop or [a virtual machine](https://openclassrooms.com/fr/courses/2035806-virtualisez-votre-architecture-et-vos-environnements-de-travail/6313946-installez-virtualbox). The [Freebox Delta/Ultra offer virtual machines](https://next.ink/3493/machines-virtuelles-et-freebox-delta-comment-heberger-votre-premiere-page-web/).
- Know how to configure [NAT rules on a router and assign DHCP leases](/general/networking/nat)
- Know how to configure the [DNS zone of a domain name](/general/networking/dns)
:::
::
<p align="center">
<img src="/img/serveex/serveex.svg" align="center" width="700">
The goal is to be easily deployable and easy to migrate, so here is its structure:
::card-grid{grid-template-columns="repeat(2, minmax(0, 1fr));"}
#title
The Core of the Server
#root
:ellipsis{left=0px width=40rem top=10rem blur=140px}
#default
::card{icon=logos:debian}
#title
__Operating System__
#description
[Install and configure Debian 13](/serveex/core/installation)
::
::card{icon=logos:docker-icon}
#title
__Container Engine__
#description
[Install Docker](/serveex/core/docker)
::
::card{icon=carbon:container-registry style="color: rgb(41, 194, 243);" }
#title
__Docker GUI__
#description
[Install and deploy Dockge](/serveex/core/docker#installer-dockge-pour-gérer-et-déployer-les-conteneurs)
::
::card{icon=noto:globe-showing-americas}
#title
__Reverse Proxy__
#description
[Expose your services with SWAG](/serveex/core/swag)
::
::
::card-grid
#title
Security
#root
:ellipsis{left=0px width=40rem top=10rem blur=140px}
#default
::card{icon=simple-icons:wireguard style="color: #88171a;"}
#title
__VPN__
#description
[Install and deploy Wireguard](/serveex/security/wireguard)
::
::card{icon=noto:key}
#title
__SSO & MFA__
#description
[Install and deploy Authentik](/serveex/security/authentik)
::
::card{icon=logos:cloudflare-icon}
#title
__Zero Trust__
#description
[Install and deploy Cloudflared](/serveex/security/cloudflare)
::
::
::card-grid
#title
Monitoring
#root
:ellipsis{left=0px width=40rem top=10rem blur=140px}
#default
::card{icon=solar:pulse-linear style="color: rgb(99, 222, 144);"}
#title
__Service Status__
#description
[Install and deploy Uptime-Kuma](/serveex/monitoring/uptime-kuma)
::
::card{icon=lucide:logs style="color: #1AD6FF;"}
#title
__Log Management__
#description
[Install and deploy Dozzle](/serveex/monitoring/dozzle)
::
::card{icon=noto:rabbit style="color: #1AD6FF;"}
#title
__Connection Management__
#description
[Install and deploy Speedtest Tracker](/serveex/monitoring/speedtest-tracker)
::
::card{icon=lucide:chart-column-decreasing style="color:rgb(26, 255, 213);"}
#title
__Resource Status__
#description
[Install and deploy Beszel](/serveex/monitoring/beszel)
::
::card{icon=lucide:circle-power style="color:rgb(228, 117, 117);"}
#title
__Wake on Lan__
#description
[Install and deploy UpSnap](/serveex/monitoring/upsnap)
::
::
::card-grid
#title
Media
#root
:ellipsis{left=0px width=40rem top=10rem blur=140px}
#default
::card{icon=cbi:plex-alt style="color: rgb(229, 160, 13);"}
#title
__Media__
#description
[Install and deploy Plex](/serveex/media/plex)
::
::card{icon=cbi:qbittorrent style="color: rgb(#2f67ba);"}
#title
__Seedbox__
#description
[Install and deploy Qbittorrent](/serveex/media/qbittorrent)
::
::
::card-grid
#title
Cloud Drive & Photos
#root
:ellipsis{left=0px width=40rem top=10rem blur=140px}
#default
::card{icon=cib:nextcloud style="color: rgb(0, 104, 161);"}
#title
__Drive__
#description
[Install and deploy Nextcloud](/serveex/cloud/nextcloud)
::
::card{icon=simple-icons:immich style="color: #ed79b5;"}
#title
__Photos__
#description
[Install and deploy Immich](/serveex/cloud/immich)
::
::
::card-grid
#title
Files & Sharing
#root
:ellipsis{left=0px width=40rem top=10rem blur=140px}
#default
::card{icon=noto:open-file-folder }
#title
__File Explorer__
#description
[Install and deploy file-browser](/serveex/files/file-browser)
::
::card{icon=carbon:share style="color: #47428e;" }
#title
__Sharing__
#description
[Install and deploy Pingvin](/serveex/files/pingvin)
::
::
::card-grid
#title
Development Tools
#root
:ellipsis{left=0px width=40rem top=10rem blur=140px}
#default
::card{icon=logos:visual-studio-code}
#title
__Visual Studio Code__
#description
[Install and deploy code-server](/serveex/development/code-server)
::
::card{icon=simple-icons:gitea style="color: #9ee773;"}
#title
__Git Repository__
#description
[Install and deploy Gitea](/serveex/development/gitea)
::
::card{icon=noto:hammer-and-wrench }
#title
__Tools__
#description
[Install and deploy IT Tools](/serveex/development/it-tools)
::
::
::card-grid
#title
Useful Applications
#root
:ellipsis{left=0px width=40rem top=10rem blur=140px}
#default
::card{icon=cbi:adguard style="color: #67b279;"}
#title
__Ad-blocking DNS and Filters__
#description
[Install and deploy Adguard Home](/serveex/apps/adguard)
::
::card{icon=cbi:bitwarden style="color: rgb(25 128 255);" }
#title
__Password Manager__
#description
[Install and deploy Vaultwarden](/serveex/apps/vaultwarden)
::
::
## Coming Soon
---
- Homepage, to have all your services at a glance and access them easily
- Mkdocs for your documentation
- Docus, an alternative to Mkdocs
- UpSnap to remotely wake your machines
@@ -1,76 +0,0 @@
---
navigation: true
title: Debian 13
description: Step-by-step guide to install Debian 13 on a home server and set up SSH access, essential packages, and a ready-to-use base system.
main:
fluid: false
---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Debian 13
::alert{type="info"}
🎯 __Goal:__ Install Debian 13 and the main dependencies to have a ready-to-use OS, accessible via SSH.
::
![picture](/img/serveex/server.svg)
## Installation
---
1. [BIOS Setup]((https://www.debian.org/releases/stable/i386/ch03s06.en.html#bios-setup)
2. [Download Debian Image](https://www.debian.org/download.en.html)
3. [Create Bootable USB (Rufus)](https://dev.to/devops2808/how-to-create-bootable-usb-installer-for-debian-12-4f66)
4. [Install Debian and Set Up SSH](https://www.howtoforge.com/tutorial/debian-minimal-server/)
5. Install sudo and add a user to the sudo group for administrative privileges.
Log in as root:
```sh
su -
```
Enter your password, then type:
```sh
apt install sudo
```
Add the user to the sudo group:
```sh
adduser <username> sudo
```
Next time the user logs in, they will be able to use the `sudo` command to execute commands with administrative privileges.
6. [Everything About Remote Console Access (SSH)](https://www.digitalocean.com/community/tutorials/ssh-essentials-working-with-ssh-servers-clients-and-keys)
7. Optional - [UPS Client in Case of Power Outage](https://www.sindastra.de/p/2078/how-to-connect-linux-server-to-synology-ups-server) / [also here](https://www.reddit.com/r/synology/comments/gtkjam/use_synology_nas_as_ups_server_to_safely_power/)
8. Optional - Wake up after power outage → configure BIOS S0 state
9. Optional - [Wake Server Remotely (WoW - WoL)](https://dev.to/zakery1369/enable-wake-on-lan-on-debian-4ljd)
## Must-Have CLI Apps
---
Some essential apps youll likely need at some point, so might as well install them early:
```sh
sudo apt update
sudo apt upgrade
sudo apt install vim btop ranger git duf neofetch samba cifs-utils tree unzip
```
Additionally:
- [gping](https://www.linode.com/docs/guides/how-to-use-gping-on-linux/) - Graphical ping tool
- [lazydocker](https://github.com/jesseduffield/lazydocker) - CLI Docker container manager
## Useful Features
---
### Firewall
- [ufw](https://www.zenarmor.com/docs/network-security-tutorials/how-to-set-up-a-firewall-with-ufw-on-debian)
- [Firewalld](https://linuxcapable.com/how-to-install-firewalld-on-debian-linux/)
### Samba Sharing (Access a Remote Network Disk)
- [Create and Access a Samba Share](/general/networking/samba)
### File Transfer via rsync
```sh
sudo rsync -avhHSP /source /destination
```
::alert{type="info" icon="exclamation-circle"}
:::list{type="info"}
- Add ` --exclude @eaDir`{lang=shell} if the source is a Synology NAS
:::
::
-2
View File
@@ -1,2 +0,0 @@
navigation.title: Server core
icon: lucide:server-cog
-2
View File
@@ -1,2 +0,0 @@
navigation.title: Security
icon: lucide:shield
@@ -1,203 +0,0 @@
---
navigation: true
title: Uptime-Kuma
description: Install Uptime-Kuma to monitor your self-hosted services uptime, set up alerts, and optionally protect the dashboard with Authentik.
main:
fluid: false
---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Uptime-Kuma
::alert{type="info"}
🎯 __Goals:__
- Install and deploy Uptime-Kuma
- Expose Uptime-Kuma
- (Optional) Protect Uptime-Kuma with Authentik
::
[Uptime-Kuma](https://github.com/louislam/uptime-kuma) is a container dedicated to service monitoring. The principle is to regularly send requests to your services to determine if they are online, and alert you if not. Uptime-Kuma is developed by the same developer as Dockge.
![picture](https://user-images.githubusercontent.com/1336778/212262296-e6205815-ad62-488c-83ec-a5b0d0689f7c.jpg)
## Installation
---
Folder structure
```sh
root
└── docker
└── uptime-kuma
├── date
└── compose.yaml
```
Open Dockge, click on `compose`, name the stack `uptime-kuma`, then copy and paste the following:
```yaml
---
services:
uptime-kuma:
image: louislam/uptime-kuma:2-slim
container_name: uptime-kuma
volumes:
- /docker/uptime-kuma/uptime-kuma-data:/app/data
ports:
- 3200:3001 # <Host Port>:<Container Port>
restart: always
```
::alert{type="success"}
__Tip:__ Add the Watchtower label to each container to automate updates
```yaml
services:
uptime-kuma:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
::
You can now access the tool via `http://yourserverip:3200`.
::alert{type="danger"}
:::list{type="danger"}
- __If it fails:__ check your firewall rules.
:::
::
## Expose with Swag
---
::alert{type="info"}
📋 __Before you begin:__
<br/><br/>
We assume you have the subdomain `stats.mydomain.com` with a `CNAME` pointing to `mydomain.com` in your [DNS zone](/general/networking/dns). And of course, [unless you're using Cloudflare Zero Trust](/serveex/security/cloudflare), port `443` of your router should point to port `443` of your server via [NAT rules](/general/networking/nat).
::
::alert{type="warning"}
:::list{type="warning"}
- Uptime-Kuma does not use multi-factor authentication. Exposing Uptime-Kuma on the internet could compromise the machines it monitors. Only do this if you're using an MFA system like [Authentik](/serveex/security/authentik/). Otherwise, dont expose it with SWAG; use a VPN like [Wireguard](/serveex/security/wireguard) instead.
:::
::
In the Swag folders, create the `stats.subdomain.conf` file.
::alert{type="success"}
✨ __Tip for those who dislike the terminal:__
you can use [File Browser](/serveex/files/file-browser) to browse and edit your files instead of using terminal commands.
::
```sh
sudo vi /docker/swag/config/nginx/proxy-confs/stats.subdomain.conf
```
Enter insert mode with `i` and paste the following config:
```nginx
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name stats.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth (requires ldap-location.conf in the location block)
#include /config/nginx/ldap-server.conf;
# enable for Authelia (requires authelia-location.conf in the location block)
#include /config/nginx/authelia-server.conf;
# enable for Authentik (requires authentik-location.conf in the location block)
#include /config/nginx/authentik-server.conf;
location / {
# enable the next two lines for http auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# enable for ldap auth (requires ldap-server.conf in the server block)
#include /config/nginx/ldap-location.conf;
# enable for Authelia (requires authelia-server.conf in the server block)
#include /config/nginx/authelia-location.conf;
# enable for Authentik (requires authentik-server.conf in the server block)
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app uptime-kuma;
set $upstream_port 3001;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Press `Esc`, then save and exit with `:x` and `Enter`.
In Dockge, edit the SWAG compose and add the Uptime-Kuma network:
```yaml
services:
swag:
container_name: # ...
# ...
networks: # Link container to custom network
# ...
- uptime-kuma # Name of the declared network
networks: # Define custom network
# ...
uptime-kuma: # Name of the declared network
name: uptime-kuma_default # Actual name of the external network
external: true # Specifies it's an external network
```
Restart the stack and wait until SWAG is fully operational.
::alert{type="info"}
:::list{type="info"}
- Here we assume that the network name of Uptime-Kuma is `uptime-kuma_default`. You can verify the connection by visiting SWAG's dashboard at `http://yourserverip:81`.
:::
::
That's it! Uptime-Kuma is now exposed, and you can access it via `https://stats.mydomain.com`.
::alert{type="success"}
✨ __Tip:__
<br/><br>
You can protect this app with Authentik by opening `stats.subdomain.conf` and uncommenting the lines:
`include /config/nginx/authentik-server.conf;`
and
`include /config/nginx/authentik-location.conf;`.
Dont forget to [create an application and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy). If you want the public stats page to be accessible without authentication:
- Edit the Uptime-Kuma provider
- In *Advanced Protocol Settings > Authenticated Paths*, enter:
```properties
^/$
^/status
^/assets/
^/assets
^/icon.svg
^/api/.*
^/upload/.*
^/metrics
::
Redeploy the stack.
Uptime-Kuma will then be publicly reachable via `https://stats.mydomain.com`.
::alert{type="success"}
__Tip:__ If you're using Authentik and don't mind exposing the admin panel to your local network, you can disable Uptime-Kuma's native authentication in its settings and rely solely on Authentik.
::
-180
View File
@@ -1,180 +0,0 @@
---
navigation: true
title: Dozzle
description: Install Dozzle to monitor Docker container logs in real time from a clean web interface, exposed via SWAG.
main:
fluid: false
---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Dozzle
::alert{type="info"}
🎯 __Goals:__
- Install Dozzle
- Expose Dozzle with Swag
::
[Dozzle](https://dozzle.dev/) is a container that lets you access logs from your other containers and display them in real time through a user-friendly interface. It's a simple way to browse logs and retrieve information from the history.
![Dozzle](https://blog.unixhost.pro/wp-content/uploads/2023/03/image-5.png)
## Installation
---
Folder structure
```sh
root
└── docker
└── dozzle
└── data
```
Open Dockge, click on `compose`, name the stack `dozzle`, then copy and paste the following:
```yaml
---
services:
dozzle:
container_name: dozzle
image: amir20/dozzle:latest
ports:
- 9135:8080
env_file:
- .env
environment:
- DOZZLE_HOSTNAME=${DOMAIN}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
```
::alert{type="success"}
__Tip:__ Add the watchtower label to each container to automate updates
```yaml
services:
dozzle:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
::
Fill in your domain name in the `.env` file, for example:
```properties
DOMAIN=dozzle.mydomain.com
```
Deploy the container. Go to `http://yourserverip:9135`. Voilà, your Dozzle web UI is up and running!
## Exposing Dozzle with Swag
---
::alert{type="warning"}
:::list{type="warning"}
- Dozzle does not use multi-factor authentication. Exposing Dozzle to the internet could compromise the connected machines. Only do this if you use a multi-factor authentication system like [Authentik](/serveex/security/authentik/). Otherwise, do not expose it with SWAG and instead use a VPN like [Wireguard](/serveex/security/wireguard).
:::
::
You may want to access Dozzle remotely and on all your devices. To do so, well expose Dozzle via Swag.
::alert{type="info"}
📋 __Before you begin:__
<br/><br/>
We assume you have created a subdomain like `dozzle.mydomain.com` in your [DNS zone](/general/networking/dns) with a `CNAME` pointing to `mydomain.com` and that, [unless you're using Cloudflare Zero Trust](/serveex/security/cloudflare), youve redirected port `443` from your router to port `443` on your server in your [NAT rules](/general/networking/nat).
::
Go to Dockge and edit the SWAG compose file to add Dozzles network:
```yaml
services:
swag:
container_name: # ...
# ...
networks: # Connects the container to a custom network
# ...
- dozzle # Network name declared in the stack
networks: # Defines the custom network
# ...
dozzle: # Network name declared in the stack
name: dozzle_default # Actual name of the external network
external: true # Indicates it's an externally defined network
```
Redeploy the stack by clicking “Deploy” and wait for SWAG to be fully operational.
::alert{type="info"}
:::list{type="info"}
- We assume the Dozzle network name is `dozzle_default`. You can verify the connection is working by visiting the SWAG dashboard at `http://yourserverip:81`.
:::
::
In the Swag folder, create the `dozzle.subdomain.conf` file.
::alert{type="success"}
✨ __Tip:__ You can use [File Browser](/serveex/files/file-browser) to browse and edit files instead of using terminal commands.
::
```sh
sudo vi /docker/swag/config/nginx/proxy-confs/dozzle.subdomain.conf
```
Enter edit mode by pressing `i` and paste the configuration below:
```nginx
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name dozzle.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth (requires ldap-location.conf in the location block)
#include /config/nginx/ldap-server.conf;
# enable for Authelia (requires authelia-location.conf in the location block)
#include /config/nginx/authelia-server.conf;
# enable for Authentik (requires authentik-location.conf in the location block)
#include /config/nginx/authentik-server.conf;
location / {
# enable the next two lines for http auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# enable for ldap auth (requires ldap-server.conf in the server block)
#include /config/nginx/ldap-location.conf;
# enable for Authelia (requires authelia-server.conf in the server block)
#include /config/nginx/authelia-location.conf;
# enable for Authentik (requires authentik-server.conf in the server block)
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app dozzle;
set $upstream_port 8080;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Press `Esc`, then save and exit by typing `:x` and pressing `Enter`.
And there you go, Dozzle is now exposed!
::alert{type="success"}
✨ You can protect this app with Authentik by opening `dozzle.subdomain.conf` and removing the `#` in front of `include /config/nginx/authentik-server.conf;`{lang=nginx} and `include /config/nginx/authentik-location.conf;`{lang=nginx}. Dont forget to [create an application and a provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
::
@@ -1,197 +0,0 @@
---
navigation: true
title: Speedtest Tracker
description: Install Speedtest Tracker to automatically measure and log your internet connection speed over time, exposed with SWAG.
main:
fluid: false
---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Speedtest Tracker
::alert{type="info"}
🎯 **Objectives:**
- Install Speedtest Tracker
- Expose Speedtest Tracker with SWAG
::
[Speedtest Tracker](https://docs.speedtest-tracker.dev/) is a container that allows you to schedule regular speed tests in order to log your server's internet connection status.
![speedtest-tracker](/img/serveex/speedtest-tracker.avif)
## Installation
---
::alert{type="info"}
:::list{type="info"}
- We will use the Docker image maintained by [LinuxServer.io](https://docs.linuxserver.io/images/docker-speedtest-tracker/)
:::
::
File structure:
```sh
root
└── docker
└── speedtest-tracker
└── data
└── config
```
In a terminal, generate a key using the following command:
```sh
echo -n 'base64:'; openssl rand -base64 32;
```
Take note of the key.
Open Dockge, click on `compose`, name the stack `speedtest-tracker`, then paste the following:
```yaml
---
services:
speedtest-tracker:
image: lscr.io/linuxserver/speedtest-tracker:latest
restart: unless-stopped
container_name: speedtest-tracker
ports:
- ${PORT}:80
environment:
- PUID=${PUID}
- PGID=${GUID}
- TZ=Europe/Paris
- APP_KEY=${API_KEY}
- DB_CONNECTION=sqlite
- SPEEDTEST_SCHEDULE=${SCHEDULE}
volumes:
- /docker/speedtest-tracker/data/config:/config
```
Find your `PUID` and `GUID` by running the following command:
```sh
id yourusername
```
In the `.env` file, set the variable `API_KEY` with the key you generated and add a cron-style test schedule, as well as your `PUID` and `GUID`, for example:
```properties
SCHEDULE=15 */6 * * * # every 6 hours
API_KEY=base64:zihejehkj8_nzhY/OjeieR= # your key
PUID=1000
GUID=1000
PORT=3225 # port to access the web UI
```
::alert{type="success"}
**Tip:** You can configure additional environment variables by referring to the [official documentation](https://docs.speedtest-tracker.dev/getting-started/environment-variables).
::
Deploy the container and go to `http://yourserverip:3225`. Log in with the account `[email protected]` and the password `password`. Dont forget to change your ID and password once logged in!
## Expose Speedtest Tracker
---
::alert{type="info"}
📋 **Prerequisites:**
We assume that you've already created a subdomain like `speedtest.yourdomain.com` in your [DNS zone](/general/networking/dns) with a `CNAME` pointing to `yourdomain.com`, and [unless youre using Cloudflare Zero Trust](/serveex/security/cloudflare), you've also forwarded port `443` from your router to port `443` of your server in your [NAT rules](/general/networking/nat).
::
Now we want to expose Speedtest Tracker to the internet so you can access it remotely. We assume you've set up the DNS `CNAME` for `speedtest.yourdomain.com` pointing to `yourdomain.com`.
::alert{type="warning"}
:::list{type="warning"}
- Speedtest Tracker does not use multi-factor authentication. Exposing it on the internet could compromise connected devices. Do so only if you use a multi-factor system like [Authentik](/serveex/security/authentik/). Otherwise, avoid using SWAG and prefer a VPN like [Wireguard](/serveex/security/wireguard).
:::
::
Open the `speedtest.subdomain.conf` file:
```sh
sudo vi /docker/swag/config/nginx/proxy-confs/speedtest.subdomain.conf
```
Configure it like this:
```nginx
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name speedtest.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# Authentication options (uncomment as needed)
#include /config/nginx/ldap-server.conf;
#include /config/nginx/authelia-server.conf;
#include /config/nginx/authentik-server.conf;
location / {
# Basic auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# Per-location authentication
#include /config/nginx/ldap-location.conf;
#include /config/nginx/authelia-location.conf;
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app speedtest-tracker;
set $upstream_port 3225;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Save and exit. The configuration will update in a few seconds.
::alert{type="info"}
:::list{type="info"}
- By default, SWAG doesnt know the name "speedtest-tracker". To allow access, you need to add Speedtest Trackers network to SWAGs `compose.yml`.
:::
::
Go to Dockge, and edit SWAGs compose to include Speedtest Trackers network:
```yaml
services:
swag:
container_name: # ...
# ...
networks:
# ...
- speedtest-tracker
networks:
# ...
speedtest-tracker:
name: speedtest-tracker_default
external: true
```
Restart the stack by clicking "Deploy" and wait for SWAG to be fully up.
::alert{type="info"}
:::list{type="info"}
- This assumes the Speedtest Tracker network is named `speedtest-tracker_default`. You can verify the connection by visiting SWAGs dashboard at `http://yourserverip:81`.
:::
::
Wait a moment, then visit `https://speedtest.yourdomain.com` in your browser — you should be redirected to Speedtest Tracker. You can check service status via the dashboard (`http://yourserverip:81` from the local network).
::alert{type="success"}
✨ You can protect this app with Authentik by opening `speedtest.subdomain.conf` and uncommenting
`include /config/nginx/authentik-server.conf;` and `include /config/nginx/authentik-location.conf;`.
Dont forget to [create an application and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
::
-251
View File
@@ -1,251 +0,0 @@
---
navigation: true
title: Beszel
description: Install Beszel to monitor server CPU, RAM, disk, and network metrics — including remote servers — with a lightweight web dashboard.
main:
fluid: false
---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Beszel
::alert{type="info"}
🎯 __Objectives:__
- Install Beszel
- Monitor the local server
- Monitor a remote server
- Expose Beszel with Swag
::
[Beszel](https://beszel.dev/) is a container that gives you real-time access to hardware information from your servers and allows historical tracking. CPU activity, disk usage, temperatures, RAM—nothing escapes your monitoring. Beszel also lets you configure notifications and alerts when your predefined thresholds are exceeded.
Beszel includes a hub with a web UI and an agent that collects data from your server or a remote server.
![Beszel](/img/serveex/beszel.png)
## Installation
---
Folder structure
```sh
root
└── docker
└── beszel
├── data
└── socket
```
Open Dockge, click `compose`, name the stack `beszel`, and paste the following:
```yaml
---
services:
beszel:
image: henrygd/beszel:latest
container_name: beszel
restart: unless-stopped
ports:
- ${PORT}:8090
volumes:
- ./data:/beszel_data
- ./socket:/beszel_socket
beszel-agent:
image: henrygd/beszel-agent:latest
container_name: beszel-agent
restart: unless-stopped
network_mode: host
volumes:
- ./socket:/beszel_socket
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
LISTEN: /beszel_socket/beszel.sock
# Do not remove quotes around the key
KEY: ${KEY}
```
::alert{type="success"}
__Tip:__ Add the Watchtower label to each container to automate updates.
```yaml
services:
beszel:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
```
::
Fill out the `.env` file, for example:
```properties
PORT=8090 # web UI port
KEY= # private key to retrieve from Beszel when adding a system
```
For the `KEY` value, you'll need to launch Beszel once to get it.
Deploy the container and go to `http://yourserverip:8090`. Your Beszel web UI is now accessible!
::alert{type="danger"}
:::list{type="danger"}
- __If it fails:__ check your firewall rules.
:::
::
### Add local server information
Now that the web UI is accessible, you need to push local server information into it. Just add a machine via the web UI and configure it like this:
![Beszel add system](/img/serveex/beszel-add.png)
Note the private key and confirm. Enter the key in your `.env` file in Dockge and redeploy the stack. Once done, your server will appear in the web UI:
![Beszel system](/img/serveex/beszel-system.png)
### Add a remote server
You can also monitor a remote server. To do so, run the agent on the remote server. Add a new machine in Beszel and fill in:
- The name displayed for your remote server
- The IP address or domain name of the remote server
- The listening port (e.g., `45876`)
Beszel will suggest a `compose.yaml` to deploy on the remote server, or you can use:
```yaml
---
services:
beszel-agent:
image: henrygd/beszel-agent
container_name: beszel-agent
restart: unless-stopped
network_mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
LISTEN: ${PORT}
KEY: ${KEY}
```
And in `.env`:
```properties
PORT=45876 # communication port between hub and remote agent
KEY= # private key from Beszel when adding the system
```
Deploy the stack on the remote server. Data will begin flowing into the web UI after a few seconds.
::alert{type="danger"}
:::list{type="danger"}
- __If it fails:__ check your firewall rules.
:::
::
## Expose Beszel with Swag
---
::alert{type="warning"}
:::list{type="warning"}
- Beszel does not support multi-factor authentication. Exposing it on the internet could compromise connected machines. Only do this if you're using a system like [Authentik](/serveex/security/authentik/). Otherwise, do not expose with SWAG—use a VPN like [Wireguard](/serveex/security/wireguard) instead.
:::
::
If you want to access Beszel remotely from all your devices, expose it using Swag.
::alert{type="info"}
📋 __Prerequisite:__
<br/><br/>
You must have created a DNS subdomain like `beszel.mydomain.com` with a `CNAME` pointing to `mydomain.com`, and—unless you're using Cloudflare Zero Trust—you must have forwarded port `443` on your router to your servers `443` port via [NAT rules](/general/networking/nat).
::
In Dockge, edit Swag's compose file and add Beszels network:
```yaml
services:
swag:
container_name: # ...
# ...
networks:
# ...
- beszel # network declared in the stack
networks:
# ...
beszel:
name: beszel_default # actual external network name
external: true
```
Redeploy the stack and wait for Swag to become fully operational.
::alert{type="info"}
:::list{type="info"}
- We assume the network name is `beszel_default`. You can check connectivity by visiting Swag's dashboard at `http://yourserverip:81`.
:::
::
In Swags config folders, create `beszel.subdomain.conf`.
::alert{type="success"}
__Tip:__ Use [File Browser](/serveex/files/file-browser) to browse and edit files instead of terminal commands.
::
```sh
sudo vi /docker/swag/config/nginx/proxy-confs/beszel.subdomain.conf
```
Press `i` to enter insert mode and paste:
```nginx
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name beszel.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth
#include /config/nginx/ldap-server.conf;
# enable for Authelia
#include /config/nginx/authelia-server.conf;
# enable for Authentik
#include /config/nginx/authentik-server.conf;
location / {
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
#include /config/nginx/ldap-location.conf;
#include /config/nginx/authelia-location.conf;
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app beszel;
set $upstream_port 8090;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Press `Esc`, type `:x`, and hit `Enter` to save and exit.
Thats it—Beszel is now exposed!
::alert{type="success"}
✨ You can protect this app with Authentik by opening `beszel.subdomain.conf` and removing the `#` in front of `include /config/nginx/authentik-server.conf;` and `include /config/nginx/authentik-location.conf;`. Dont forget to [create an application and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
::
-195
View File
@@ -1,195 +0,0 @@
---
navigation: true
title: UpSnap
description: Install UpSnap to remotely wake up machines on your local network via Wake-on-LAN, exposed with SWAG.
main:
fluid: false
---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# UpSnap
::alert{type="info"}
🎯 __Goals:__
- Install UpSnap
- Expose UpSnap with Swag
::
[UpSnap](https://github.com/seriousm4x/UpSnap) is a container that allows you to remotely power on, shut down, or put your machines to sleep. It mainly uses Wake-On-Lan (WoL) over the network and offers advanced features.
![Beszel](/img/serveex/upsnap.webp)
## Installation
---
Folder structure
```sh
root
└── docker
└── upsnap
└── data
```
Open Dockge, click on `compose`, name the stack `upsnap`, then copy and paste the following:
```yaml
---
services:
upsnap:
container_name: upsnap
image: ghcr.io/seriousm4x/upsnap:5
network_mode: host
restart: unless-stopped
volumes:
- /docker/upsnap/data:/app/pb_data
environment:
- TZ=Europe/Paris
- UPSNAP_SCAN_RANGE=${SCAN_RANGE}
- UPSNAP_SCAN_TIMEOUT=500ms
- UPSNAP_PING_PRIVILEGED=true
dns:
- ${DNS}
entrypoint: /bin/sh -c "./upsnap serve --http 0.0.0.0:8095"
healthcheck:
test: curl -fs "http://localhost:8095/api/health" || exit 1
interval: 10s
```
::alert{type="success"}
__Tip:__ Add the watchtower label to each container to automate updates
```yaml
services:
upsnap:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
::
Fill in the `.env`, for example:
```properties
RANGE=192.168.1.0/24 # scans all devices on the local network with an IP between 192.168.0.1 and 192.168.1.255
DNS=192.168.1.1 # DNS IP to resolve domain names, typically your routers IP
```
Deploy the container and go to `http://yourserverip:8095`. Just follow the steps to create your account!
::alert{type="danger"}
:::list{type="danger"}
- __If it fails:__ check your firewall rules.
:::
::
## Exposing UpSnap with Swag
---
::alert{type="warning"}
:::list{type="warning"}
- UpSnap does not support multi-factor authentication. Exposing it on the internet could compromise connected machines. Do this only if you're using a multi-factor authentication system like [Authentik](/serveex/security/authentik/). Otherwise, avoid exposing it with SWAG and use a VPN like [Wireguard](/serveex/security/wireguard) instead.
:::
::
You may want to access it remotely from all your devices. To do so, we'll expose UpSnap via Swag.
::alert{type="info"}
📋 __Beforehand:__
<br/><br/>
We assume you've created a subdomain in your [DNS zone](/general/networking/dns), such as `upsnap.yourdomain.com` with a `CNAME` to `yourdomain.com`. Also, unless you're using Cloudflare Zero Trust, you should have already forwarded port `443` from your router to port `443` on your server in your [NAT rules](/general/networking/nat).
::
Go to Dockge, and edit the SWAG compose by adding the UpSnap network:
```yaml
services:
swag:
container_name: # ...
# ...
networks: # Connects the container to the custom network
# ...
- upsnap # Network name declared in the stack
networks: # Defines the custom network
# ...
upsnap: # Network name declared in the stack
name: upsnap_default # Actual name of the external network
external: true # Indicates it's an external network
```
Restart the stack by clicking "deploy" and wait for SWAG to be fully operational.
::alert{type="info"}
:::list{type="info"}
- Here we assume the network name for upsnap is `upsnap_default`. You can check the connection in the SWAG dashboard at `http://yourserverip:81`.
:::
::
In the Swag folders, create the file `upsnap.subdomain.conf`.
::alert{type="success"}
✨ __Tip:__ You can use [File Browser](/serveex/files/file-browser) to navigate your files and edit documents instead of using terminal commands.
::
```sh
sudo vi /docker/swag/config/nginx/proxy-confs/upsnap.subdomain.conf
```
Enter edit mode by pressing `i`, and paste the following configuration:
```nginx
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name upsnap.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth (requires ldap-location.conf in the location block)
#include /config/nginx/ldap-server.conf;
# enable for Authelia (requires authelia-location.conf in the location block)
#include /config/nginx/authelia-server.conf;
# enable for Authentik (requires authentik-location.conf in the location block)
#include /config/nginx/authentik-server.conf;
location / {
# enable the next two lines for http auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# enable for ldap auth (requires ldap-server.conf in the server block)
#include /config/nginx/ldap-location.conf;
# enable for Authelia (requires authelia-server.conf in the server block)
#include /config/nginx/authelia-location.conf;
# enable for Authentik (requires authentik-server.conf in the server block)
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app upsnap;
set $upstream_port 8095;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Press `Escape`, then save and exit by typing `:x` and pressing `Enter`.
And thats it — youve exposed UpSnap!
::alert{type="success"}
✨ You can protect this app with Authentik by opening `upsnap.subdomain.conf` and removing the `#` in front of `include /config/nginx/authentik-server.conf;`{lang=nginx} and `include /config/nginx/authentik-location.conf;`{lang=nginx}. Dont forget to [create an application and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
::
-2
View File
@@ -1,2 +0,0 @@
navigation.title: Monitoring
icon: lucide:chart-no-axes-column
-2
View File
@@ -1,2 +0,0 @@
navigation.title: Media & Seedbox
icon: lucide:list-video
-199
View File
@@ -1,199 +0,0 @@
---
navigation: true
title: Nextcloud
description: Install Nextcloud to self-host your files, photos, and calendar — a privacy-friendly alternative to Google Drive, OneDrive, and iCloud.
main:
fluid: false
---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Nextcloud
::alert{type="info"}
🎯 __Goals:__ Install [Nextcloud](https://nextcloud.com/) to manage your photos and files across all your devices.
::
[Nextcloud](https://nextcloud.com/) is a self-hosted solution that allows you to access and synchronize your data across all your devices. It also includes collaboration features, calendar, and more. Its a great alternative to services like Google Drive, iCloud, or OneDrive.
![Picture](/img/serveex/nextcloud.png)
## Installation
---
::alert{type="info"}
:::list{type="info"}
- We'll be using the Docker image maintained by [LinuxServer.io](https://docs.linuxserver.io/images/docker-nextcloud/)
:::
::
File structure:
```sh
root
└── docker
└── nextcloud
├── config
├── data
├── compose.yaml
└── .env
```
Open Dockge, click on `compose`, name the stack `nextcloud` and paste the following:
```yaml
---
services:
nextcloud:
image: lscr.io/linuxserver/nextcloud:latest
container_name: nextcloud
environment:
- PUID=${PUID}
- PGID=${GUID}
- TZ=Etc/UTC
volumes:
- /docker/nextcloud/config:/config
- /docker/nextcloud/data:/data
ports:
- ${PORT}:443
restart: unless-stopped
```
::alert{type="info"}
:::list{type="info"}
- If youre using a NAS or network-shared drive via [Samba](/general/networking/samba), replace `/docker/nextcloud/data` with the path to your shared folder.
:::
::
Find your `PUID` and `GUID` by running the following command:
```sh
id username
```
Then fill out the `.env` file with your preferred port and the values found above, for example:
```properties
PUID=1000
GUID=1000
PORT=4545
```
Deploy the stack and visit `http://yourserverip:4545` to complete the setup.
::alert{type="danger"}
:::list{type="danger"}
- __If it fails:__ check your firewall rules.
:::
::
## Exposing Nextcloud with Swag
---
The goal of this setup is to access Nextcloud remotely from all your devices. Well use Swag to expose the app.
::alert{type="info"}
:::list{type="info"}
- We assume you have a subdomain `nextcloud.yourdomain.com` with a `CNAME` pointing to `yourdomain.com` in your [DNS zone](/general/networking/dns). And unless youre using [Cloudflare Zero Trust](/serveex/security/cloudflare), port `443` on your router must be forwarded to port `443` on your server using [NAT rules](/general/networking/nat).
:::
::
In Dockge, go to your SWAG stack and edit the compose to add Nextcloud's network:
```yaml
services:
swag:
container_name: # ...
# ...
networks:
# ...
- nextcloud
networks:
# ...
nextcloud:
name: nextcloud_default
external: true
```
::alert{type="info"}
:::list{type="info"}
- We assume the Nextcloud network is named `nextcloud_default`. You can confirm connectivity by visiting the SWAG dashboard at http://yourserverip:81.
:::
::
Redeploy the stack and wait for SWAG to become fully operational.
In Nextclouds files, edit the `config.php` file:
::alert{type="success"}
__Tip:__ You can use [File Browser](/serveex/files/file-browser) to navigate and edit files instead of using terminal commands.
::
```sh
sudo vi /docker/nextcloud/config/www/nextcloud/config/config.php
```
Enter edit mode with `i` and paste the following before the final `);`:
```php
'trusted_proxies' => [gethostbyname('swag')],
'overwrite.cli.url' => 'https://nextcloud.example.com/',
'overwritehost' => 'nextcloud.example.com',
'overwriteprotocol' => 'https',
```
Also add your domain in the `array` section. It should look like this:
```php
array (
0 => '192.168.0.1:444', # This line may differ—dont change it!
1 => 'nextcloud.yourdomain.com', # Add your domain here
),
```
Press `Esc`, then save and exit by typing `:x` and hitting Enter.
In Swags folders, create the file `nextcloud.subdomain.conf`:
```sh
sudo vi /docker/swag/config/nginx/proxy-confs/nextcloud.subdomain.conf
```
Enter edit mode with `i` and paste the following:
```nginx
## Version 2024/04/25
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name nextcloud.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
location / {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app nextcloud;
set $upstream_port 443;
set $upstream_proto https;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
# Hide proxy response headers from Nextcloud that conflict with ssl.conf
proxy_hide_header Referrer-Policy;
proxy_hide_header X-Content-Type-Options;
proxy_hide_header X-Frame-Options;
proxy_hide_header X-XSS-Protection;
# Disable proxy buffering
proxy_buffering off;
}
}
```
Press `Esc`, save and exit with `:x` then Enter.
Thats it—youve exposed Nextcloud! Dont forget to install [the desktop and mobile apps](https://nextcloud.com/install/).
::alert{type="success"}
__Tip:__ You can natively protect this app with Authentik by [following these instructions](https://docs.goauthentik.io/integrations/services/nextcloud/).
::
-2
View File
@@ -1,2 +0,0 @@
navigation.title: Cloud Drive & Photos
icon: lucide:cloud-upload
-164
View File
@@ -1,164 +0,0 @@
---
navigation: true
title: File Browser
description: Install File Browser to browse and manage your server files from a web interface, exposed securely with SWAG.
main:
fluid: false
---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# File Browser
::alert{type="info"}
🎯 __Objectives:__
- Install File Browser
- Expose File Browser using Swag
::
[File Browser](https://github.com/filebrowser/filebrowser) is a web-based interface that lets you access and edit the files on your server.
![File Browser](/img/serveex/filebrowser.png)
## Installation
---
Open Dockge, click on `compose`, name the stack `filebrowser`, then copy and paste the following:
```yaml
---
services:
filebrowser:
container_name: filebrowser
volumes:
- /docker/filebrowser/config:/config/
- /path/to/your/folders:/yourfolders #add your folders to browse as /docker:/docker for exemple
ports:
- 8010:80
image: filebrowser/filebrowser:s6
```
::alert{type="success"}
__Tip:__ Add the watchtower label to each container to automate updates.
```yaml
services:
filebrowser:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
::
Deploy the container and go to `http://yourserverip:8010`. Thats it—your File Browser web UI is up and running!
::alert{type="danger"}
:::list{type="danger"}
- __If it doesnt work:__ check your firewall rules.
:::
::
## Exposing File Browser with Swag
---
::alert{type="warning"}
:::list{type="warning"}
- File Browser does not support multi-factor authentication. Exposing it publicly could put your systems at risk. Only do this if youre using a secure authentication solution like [Authentik](/serveex/security/authentik/). Otherwise, do not expose it with SWAG—use a VPN like [Wireguard](/serveex/security/wireguard) instead.
:::
::
You may want to access File Browser remotely from all your devices. To do that, well expose it through Swag.
::alert{type="info"}
:::list{type="info"}
- __Pre-requisite:__ We assume you've already created a subdomain like `files.yourdomain.com` in your [DNS zone](/general/networking/dns) pointing to `yourdomain.com` with a `CNAME`, and—unless you're using Cloudflare Zero Trust—have already forwarded port `443` on your router to port `443` on your server using [NAT rules](/general/networking/nat).
:::
::
In Dockge, go to the SWAG stack and edit the compose file to add File Browsers network:
```yaml
services:
swag:
container_name: # ...
# ...
networks: # Connects the container to the custom network
# ...
- filebrowser # Name of the network declared in the stack
networks: # Defines the custom network
# ...
filebrowser: # Name of the network declared in the stack
name: filebrowser_default # Actual name of the external network
external: true # Specifies it's an external network
```
::alert{type="info"}
:::list{type="info"}
- Here, we assume the network name for File Browser is `filebrowser_default`. You can confirm the connection is working by accessing the SWAG dashboard at http://yourserverip:81.
:::
::
Restart the stack by clicking "deploy" and wait for SWAG to fully initialize.
In the Swag folders, create the file `files.subdomain.conf`.
```sh
sudo vi /docker/swag/config/nginx/proxy-confs/files.subdomain.conf
```
Enter insert mode by pressing `i`, and paste the following configuration:
```nginx
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name files.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth (requires ldap-location.conf in the location block)
#include /config/nginx/ldap-server.conf;
# enable for Authelia (requires authelia-location.conf in the location block)
#include /config/nginx/authelia-server.conf;
# enable for Authentik (requires authentik-location.conf in the location block)
#include /config/nginx/authentik-server.conf;
location / {
# enable the next two lines for http auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# enable for ldap auth (requires ldap-server.conf in the server block)
#include /config/nginx/ldap-location.conf;
# enable for Authelia (requires authelia-server.conf in the server block)
#include /config/nginx/authelia-location.conf;
# enable for Authentik (requires authentik-server.conf in the server block)
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app filebrowser;
set $upstream_port 80;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Press `Esc`, then save and exit with `:x` followed by `Enter`.
Thats it—File Browser is now exposed!
::alert{type="success"}
✨ __Tip:__ You can protect this app with Authentik by opening `files.subdomain.conf` and uncommenting `include /config/nginx/authentik-server.conf;`{lang=nginx} and `include /config/nginx/authentik-location.conf;`{lang=nginx}. Dont forget to [create an application and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
::
-209
View File
@@ -1,209 +0,0 @@
---
navigation: true
title: Pingvin
description: Install Pingvin Share, a self-hosted file sharing platform to send files securely without relying on WeTransfer or Google Drive.
main:
fluid: false
---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Pingvin
::alert{type="info"}
🎯 __Objectifs :__
- Installer Pingvin
- Exposer Pingvin
::
[Pingvin](https://github.com/stonith404/pingvin-share) est un outil permettant de partager rapidement des fichiers, à la manière de WeTransfer. Ses nombreuses options de partage (mot de passe, durée d'expiration, personnalisation du lien...) en font l'outil idéal pour partager rapidement des fichiers. Pingvin permet également de créer des _demandes de dépot_, c'est à dire un lien partageable à envoyer à quelqu'un de votre choix pour qu'il puisse téléverser ses fichiers afin que vous puissiez les récupérer.
![File Browser](/img/serveex/pingvin.png)
## Installation
---
Ouvrez Dockge, cliquez sur `compose`, appelez la stack `pingvin` puis copiez collez ceci :
```yaml
---
services:
pingvin-share:
container_name: pingvin
image: stonith404/pingvin-share
restart: unless-stopped
ports:
- 3600:3000
volumes:
- /docker/pingvin/data:/opt/app/backend/data
- /docker/pingvin/data/img:/opt/app/frontend/public/img
- /docker/pingvin/uploads:/opt/app/backend/uploads # chemin du dossier dans lequel vous souhaitez stocker les fichiers uploadés dans pingvin. A changer selon vos préférences.
depends_on:
clamav:
condition: service_healthy
networks:
- swag
clamav: #antivirus pour les fichiers
restart: unless-stopped
image: clamav/clamav
```
::alert{type="info"}
:::list{type="info"}
- Ici nous partons du principe que le nom du réseau de Swag est `swag_default`.
:::
::
::alert{type="success"}
__Astuce :__ ajoutez le label de watchtower dans chaque conteneur afin d'automatiser les mises à jour
```yaml
services:
filebrowser:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
clamav:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
::
Déployez le conteneur et rendez-vous sur `http://ipduserveur:3600`. Et voilà, votre instance File Browser en webui est disponible !
::alert{type="danger"}
:::list{type="danger"}
- __En cas d'échec :__ vérifiez les règles de votre pare-feu.
:::
::
## Exposer Immich avec Swag
---
Tout l'intérêt d'une telle solution, c'est de pouvoir y accéder à distance et sur tout vos appareils. Pour cela, nous allons exposer Pingvin via Swag.
::alert{type="info"}
📋 __Au préalable :__
<br/><br/>
Nous partons du principe que vous avez le sous-domaine `pingvin.mondomaine.fr` avec un `CNAME` qui pointe vers `mondomaine.fr` dans votre [zone DNS](/general/networking/dns). Et que bien sûr, [à moins que vous utilisiez Cloudflare Zero Trust](/serveex/security/cloudflare), le port `443` de votre box pointe bien sur le port `443` de votre serveur via [les règles NAT](/general/networking/nat).
::
Dans Dockge, rendez-vous dans la stack de SWAG et éditez le compose en ajoutant le réseau de pingvin :
```yaml
services:
swag:
container_name: # ...
# ...
networks: # Relie le conteneur au réseau custom
# ...
- pingvin # Nom du réseau déclaré dans la stack
networks: # Définit le réseau custom
# ...
pingvin: # Nom du réseau déclaré dans la stack
name: pingvin_default # Nom véritable du réseau externe
external: true # Précise que c'est un réseau à rechercher en externe
```
::alert{type="info"}
:::list{type="info"}
- Ici nous partons du principe que le nom du réseau de pingvin est `pingvin_default`. Vous pouvez vérifier que la connexion est opérationnelle en visitant le dashboard de SWAG en tapant http://ipduserveur:81.
:::
::
Relancez la stack en cliquant sur "déployer" et patientez le temps que SWAG soit complètement opérationnel.
Dans les dossiers de Swag, créez le fichier `pingvin.subdomain.conf`.
::alert{type="success"}
:::list{type="success"}
- __Astuce :__ vous pouvez utiliser [File Browser](/serveex/files/file-browser) pour naviguer dans vos fichier et éditer vos documents au lieu d'utiliser les commandes du terminal.
:::
::
```sh
sudo vi /docker/swag/config/nginx/proxy-confs/pingvin.subdomain.conf
```
Entrez en modification avec la touche `i` et collez la configuration ci-dessous :
```nginx
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name pingvin.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth (requires ldap-location.conf in the location block)
#include /config/nginx/ldap-server.conf;
# enable for Authelia (requires authelia-location.conf in the location block)
#include /config/nginx/authelia-server.conf;
# enable for Authentik (requires authentik-location.conf in the location block)
#include /config/nginx/authentik-server.conf;
location / {
# enable the next two lines for http auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# enable for ldap auth (requires ldap-server.conf in the server block)
#include /config/nginx/ldap-location.conf;
# enable for Authelia (requires authelia-server.conf in the server block)
#include /config/nginx/authelia-location.conf;
# enable for Authentik (requires authentik-server.conf in the server block)
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app pingvin;
set $upstream_port 3000;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Appuyez sur `Echap puis sauvegardez et quittez en tapant `:x` puis en appuyant sur `Entrée`.
Et voilà, vous avez exposé Pingvin !
## Sécuriser Pingvin avec Authentik
Vous pouvez protéger cette app avec Authentik de façon native en suivant les instructions ci-dessous.
1. Dans votre espace admin authentik, créez un fournisseur OAuth2/OpenID.
2. Remplissez chaque section comme suit en remplaçant `mondomaine.fr` par votre domaine. Copiez quelque part le contenu des champs `ID du client` et `Secret du client`.
![Picture](/img/serveex/pingvin-auth1.png)
![Picture](/img/serveex/pingvin-auth2.png)
![Picture](/img/serveex/pingvin-auth3.png)
3. Enregistrez et créez une application `pingvin` comme suit.
![Picture](/img/serveex/pingvin-auth4.png)
4. Enregistrez et aller dans la liste de vos avant-postes. Ajoutez le fournisseur pingvin` à votre avant-poste.
5. Quittez authentik, et allez dans l'interface d'administration de Pingvin.
6. Dans la section _« Identifiant social »_ renseignez les champs suivant :
- `URI de découverte OpenID` avec `https://pingvin.mondomaine.fr/application/o/pingvin/.well-known/openid-configuration` (n'oubliez pas de remplacer `mondomaine.fr` par votre domaine)
- `Revendication du nom dutilisateur OpenID` avec `preferred_username`
- `ID du client OpenID` avec l'ID que vous avez copié en étape 2.
- `Secret du client OpenID` avec le token que vous avez copié en étape 2.
Et voilà, désormais lorsque vous vous connectez à Pingvin, un bouton "Open ID" sera disponible en dessous de la mire de connexion.
-2
View File
@@ -1,2 +0,0 @@
navigation.title: File & share
icon: lucide:folder-tree
@@ -1,224 +0,0 @@
---
navigation: true
title: Code-Server
description: Install code-server to run VS Code in your browser from your homelab — mount folders and expose it securely with SWAG.
main:
fluid: false
---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Code-Server
::alert{type="info"}
🎯 __Goals:__
- Install code-server
- Mount folders into VS Code
- Expose code-server with Swag
::
[code-server](https://github.com/linuxserver/docker-code-server) is a container that lets you access [VS Code](https://code.visualstudio.com/) via a web UI in a Linux environment. It's literally VS Code and your projects in your pocket, available anywhere.
![code-server](https://github.com/coder/code-server/raw/main/docs/assets/screenshot-2.png)
## Installation
---
::alert{type="info"}
:::list{type="info"}
- For this setup, well use the [image maintained by LinuxServer.io](https://docs.linuxserver.io/images/docker-code-server/).
:::
::
Folder structure
```sh
root
├── docker
│ └── code-server
│ └── config
└── #any folder you want to mount in VS Code
```
Open Dockge, click on `compose`, name the stack `code-server`, and paste the following:
```yaml
---
services:
code-server:
image: lscr.io/linuxserver/code-server:latest
container_name: code-server
environment:
- PUID=${PUID}
- PGID=${GUID}
- TZ=Etc/UTC
- HASHED_PASSWORD=${PW}
volumes:
- /docker/code-server/config:/config
# add folders to mount in VS Code
# - /path/to/folder:/folder
ports:
- 8443:8443
restart: unless-stopped
```
::alert{type="success"}
✨ Add the Watchtower label to each container to automate updates
```yaml
services:
code-server:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
::
Choose a password and generate its hash:
```sh
echo -n "yourpassword" | npx argon2-cli -e
```
Save the result carefully. Find your PUID and GUID with:
```sh
id yourusername
```
Fill in the `.env` file with the values you found, for example:
```properties
PW='$argon2i$v=19$m=4096,t=3,p=1$wST5QhBgk2lu1ih4DMuxvg$LS1alrVdIWtvZHwnzCM1DUGg+5DTO3Dt1d5v9XtLws4'
PUID=1000
GUID=1000
```
::alert{type="warning"}
:::list{type="warning"}
- __Note:__ Make sure to wrap the hash in single quotes `'`
:::
::
Deploy the container and go to `http://yourserverip:8443`. Voilà, your code-server instance is up and running in the browser!
::alert{type="danger"}
:::list{type="danger"}
- __If it fails:__ check your firewall rules.
:::
::
## Mount Folders
---
You can mount folders into VS Code by adding the relevant volumes in `compose.yaml` (or via Dockge), then redeploy the container.
```yaml
services:
code-server:
#...
volumes:
- /path/to/folder:/folder
```
Once inside VS Code, you'll have access to the mounted folder.
## Expose code-server with Swag
---
The whole point of such a solution is to access it remotely from any device. To do this, well expose code-server via Swag.
::alert{type="info"}
:::list{type="info"}
- __Preliminary:__ We assume youve created a subdomain like `code.yourdomain.com` with a `CNAME` pointing to `yourdomain.com` in your [DNS zone](/general/networking/dns), and—unless you're using [Cloudflare Zero Trust](/serveex/security/cloudflare)—that youve forwarded port `443` from your router to port `443` on your server using [NAT rules](/general/networking/nat).
:::
::
In Dockge, go to the SWAG stack and edit the compose file to add code-servers network:
```yaml
services:
swag:
container_name: # ...
# ...
networks: # Connects the container to a custom network
# ...
- code-server # Name of the network defined in the stack
networks: # Defines the custom network
# ...
code-server: # Name of the network defined in the stack
name: code-serveur # Actual name of the external network
external: true # Indicates its an external network
```
::alert{type="info"}
:::list{type="info"}
- We assume the network name is `code-server_default`. You can verify that the connection works by visiting the SWAG dashboard at http://yourserverip:81.
:::
::
Redeploy the stack by clicking “deploy” and wait until SWAG is fully operational.
Inside the Swag config folders, create the file `code.subdomain.conf`.
::alert{type="success"}
✨ __Tip:__ You can use [File Browser](/serveex/files/file-browser) to navigate and edit your files instead of using terminal commands.
::
```sh
sudo vi /docker/swag/config/nginx/proxy-confs/code.subdomain.conf
```
Enter insert mode with `i` and paste the following configuration:
```nginx
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name code.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth (requires ldap-location.conf in the location block)
#include /config/nginx/ldap-server.conf;
# enable for Authelia (requires authelia-location.conf in the location block)
#include /config/nginx/authelia-server.conf;
# enable for Authentik (requires authentik-location.conf in the location block)
#include /config/nginx/authentik-server.conf;
location / {
# enable the next two lines for http auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# enable for ldap auth (requires ldap-server.conf in the server block)
#include /config/nginx/ldap-location.conf;
# enable for Authelia (requires authelia-server.conf in the server block)
#include /config/nginx/authelia-location.conf;
# enable for Authentik (requires authentik-server.conf in the server block)
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app code-server;
set $upstream_port 8443;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Press `Esc`, then save and exit by typing `:x` and pressing `Enter`.
Thats it — code-server is now exposed!
::alert{type="success"}
✨ __Tip:__ You can protect this app with Authentik by opening `code.subdomain.conf` and uncommenting the lines `include /config/nginx/authentik-server.conf;` and `include /config/nginx/authentik-location.conf;`. Dont forget to [create an application and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
::
@@ -1,167 +0,0 @@
---
navigation: true
title: IT Tools
description: Install IT Tools, a self-hosted collection of handy utilities for developers — converters, encoders, formatters, and more.
main:
fluid: false
---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# IT Tools
::alert{type="info"}
🎯 __Goals:__
- Install IT Tools
- Expose IT Tools with Swag
::
[IT Tools](https://github.com/CorentinTh/it-tools) is a container exposing a web page that provides access to a wide range of development tools.
![IT Tools](/img/serveex/it-tools.png)
## Installation
---
Open Dockge, click on `compose`, name the stack `it-tools`, and paste the following:
```yaml
---
services:
it-tools:
container_name: it-tools
restart: unless-stopped
image: corentinth/it-tools:latest
ports:
- 3222:80
```
::alert{type="success"}
__Tip:__ Add the Watchtower label to each container to enable automatic updates.
```yaml
services:
it-tools:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
::
Deploy the container and visit `http://yourserverip:3222`. Thats it, your IT Tools web UI instance is up and running!
::alert{type="danger"}
:::list{type="danger"}
- __If it fails:__ check your firewall rules.
:::
::
## Expose IT Tools with Swag
---
You might want to access it remotely on all your devices. To do that, we'll expose IT Tools using Swag.
::alert{type="info"}
:::list{type="info"}
- __Pre-requisite:__ We assume youve created a subdomain like `tools.yourdomain.com` in your [DNS zone](/general/networking/dns) with `CNAME` set to `yourdomain.com`. Also, unless youre using [Cloudflare Zero Trust](/serveex/security/cloudflare), make sure youve already forwarded port `443` from your router to port `443` on your server in the [NAT rules](/general/networking/nat).
:::
::
In Dockge, go to the SWAG stack and edit the compose file to add the IT Tools network:
```yaml
services:
swag:
container_name: # ...
# ...
networks: # Connects the container to the custom network
# ...
- it-tools # Network name as defined in the IT Tools stack
networks: # Defines the custom network
# ...
it-tools: # Network name as defined in the IT Tools stack
name: it-tools_default # Actual name of the external network
external: true # Indicates it's an external network
```
::alert{type="info"}
:::list{type="info"}
- We assume the IT Tools network is named `it-tools_default`. You can check connectivity by visiting the SWAG dashboard at http://yourserverip:81.
:::
::
::alert{type="info"}
:::list{type="info"}
- We also assume the SWAG network is named `swag_default`.
:::
::
Restart the stack by clicking "deploy" and wait for SWAG to be fully operational.
Inside the Swag folders, create the file `tools.subdomain.conf`.
::alert{type="success"}
✨ __Tip:__ You can use [File Browser](/serveex/files/file-browser) to navigate and edit your files instead of using terminal commands.
::
```sh
sudo vi /docker/swag/config/nginx/proxy-confs/tools.subdomain.conf
```
Enter edit mode by pressing `i` and paste the configuration below:
```nginx
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name tools.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth (requires ldap-location.conf in the location block)
#include /config/nginx/ldap-server.conf;
# enable for Authelia (requires authelia-location.conf in the location block)
#include /config/nginx/authelia-server.conf;
# enable for Authentik (requires authentik-location.conf in the location block)
#include /config/nginx/authentik-server.conf;
location / {
# enable the next two lines for http auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# enable for ldap auth (requires ldap-server.conf in the server block)
#include /config/nginx/ldap-location.conf;
# enable for Authelia (requires authelia-server.conf in the server block)
#include /config/nginx/authelia-location.conf;
# enable for Authentik (requires authentik-server.conf in the server block)
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app it-tools;
set $upstream_port 80;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Press `Esc`, then save and exit by typing `:x` and pressing `Enter`.
And thats it — IT Tools is now exposed!
::alert{type="success"}
✨ __Tip:__ You can secure this app with Authentik by opening `tools.subdomain.conf` and uncommenting the lines `include /config/nginx/authentik-server.conf;` and `include /config/nginx/authentik-location.conf;`. Dont forget to [create an application and a provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
::
-2
View File
@@ -1,2 +0,0 @@
navigation.title: Developpement
icon: lucide:code-xml
-2
View File
@@ -1,2 +0,0 @@
navigation.title: Useful Apps
icon: lucide:award
-2
View File
@@ -1,2 +0,0 @@
icon: noto:microscope
navigation.redirect: /serveex/introduction
-24
View File
@@ -1,24 +0,0 @@
---
icon: lucide:bookmark
navigation: true
title: Introduction
description: Introduction to Stockeex — a personal project for stock and inventory management. Documentation coming soon.
main:
fluid: false
---
# Stockeex
::terminal{style="margin-top:80px;"}
---
content:
- sudo systemctl status stockeex-article
- currently writing, come back later...
---
::
:ellipsis{left=0px width=40rem top=10rem blur=140px}
<div align="center">
<img src="/img/stockeex/stockeex-raid.svg" alt="Image" style="max-width: 60%;">
</div>
-2
View File
@@ -1,2 +0,0 @@
icon: noto:computer-disk
navigation.redirect: /stockeex/introduction
-2
View File
@@ -1,2 +0,0 @@
navigation.title: Python
icon: lucide:file-code-2
-2
View File
@@ -1,2 +0,0 @@
navigation.title: Bash
icon: lucide:file-terminal
-2
View File
@@ -1,2 +0,0 @@
icon: noto:test-tube
navigation.title: My nonsense
-2
View File
@@ -1,2 +0,0 @@
icon: lucide:trash-2
navigation.title: Deprecated
-2
View File
@@ -1,2 +0,0 @@
icon: noto:recycling-symbol
navigation.title: Recycled
+2
View File
@@ -0,0 +1,2 @@
title: About
icon: i-noto-star
+51
View File
@@ -0,0 +1,51 @@
---
title: Welcome
description: Introduction to Docudjeex, a personal homelab documentation site covering self-hosted services, Debian, and Docker infrastructure.
navigation:
icon: i-lucide-home
hideHeader: true
hideCopyPage: true
hideToc: true
---
:ellipsis{right=0px width=75% blur=150px zIndex=60}
# docu[·]{style="color: #1ad6ff"}what?
__Docu[·]{style="color: #1ad6ff"}djeex__ is a site containing the documentation of my personal servers, originally created to easily keep track of my configurations and commands.
My infrastructure is built around the Debian 13 + Docker combo, making exporting and deployment simpler.
Special thanks to __Nipah__, __Xenio__, __KevOut__ and others for their patience and support. The simple idea of writing this documentation would not exist without them.
## About the documentation
The documentation provided here is experimental and shared in a spirit of open knowledge and experience. It is not intended to build production-grade or industrialized infrastructure. It may contain mistakes and/or approximations.
Naturally, this documentation should only be used within a strictly legal framework.
### Available or Upcoming Documentation
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card{icon="i-noto-open-book" title="General" to="/general/networking/nat"}
Networking, storage, and hardware basics
::
::card{icon="i-noto-microscope" title="Serveex" to="/serveex/introduction"}
Step-by-step Homelab Deployment Guide
::
::card{icon="i-noto-computer-disk" title="Stockeex"}
*(coming soon)* Build your own home NAS to store your data and media
::
::card{icon="i-noto-test-tube" title="My nonsense" to="/nonsense/python/nvidia-stock-bot"}
Personal scripts and side projects
::
::card{icon="i-noto-recycling-symbol" title="Recycled" to="/recycled/deprecated/wireguard-14"}
Deprecated pages, kept for archive
::
::
+2
View File
@@ -0,0 +1,2 @@
title: General
icon: i-noto-open-book
+88
View File
@@ -0,0 +1,88 @@
---
title: General
description: General homelab knowledge, networking, storage, and hardware fundamentals that apply beyond any single self-hosted app.
navigation:
icon: i-lucide-bookmark
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
## Homelab Fundamentals
This section covers the general knowledge that [Serveex](/serveex/introduction) itself relies on but doesn't re-explain every time: how networking actually works at home, how to choose and set up storage, and what hardware to run it all on. Read it once, then link back to it from any app-specific guide.
### Networking
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card{icon="i-lucide-router" title="NAT & DHCP" to="/general/networking/nat"}
Port forwarding and fixed DHCP leases on your router
::
::card{icon="i-lucide-globe" title="DNS Zone" to="/general/networking/dns"}
Reading and editing a domain's DNS zone
::
::card{icon="i-lucide-folder-sync" title="Samba" to="/general/networking/samba"}
Share folders over the local network
::
::
### Storage
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card{icon="i-lucide-database" title="RAID" to="/general/storage/raid"}
Redundant disk arrays, hardware vs software
::
::card{icon="i-lucide-layers" title="ZFS" to="/general/storage/zfs"}
Snapshots, checksums, and built-in redundancy
::
::
### Hardware
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card{icon="i-lucide-cpu" title="The Basics" to="/general/hardware/basics"}
CPUs, RAM, storage, and form factors
::
::card{icon="i-lucide-network" title="Network" to="/general/hardware/network"}
Switches, NICs, and cabling
::
::card{icon="i-lucide-hard-drive" title="The ProloNAS" to="/general/hardware/prolonas"}
A budget N100 home server build
::
::
### Linux tips for dummies
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card{icon="i-lucide-terminal" title="Command Line Basics" to="/general/linux/cli-basics"}
How a command is built, and the ones you'll actually use
::
::card{icon="i-lucide-folder-tree" title="Folders and Partitions" to="/general/linux/filesystem"}
What lives where on Debian, and the habits that keep it tidy
::
::card{icon="i-lucide-wrench" title="Handy CLI Tools" to="/general/linux/handy-tools"}
Terminal tools worth installing, and how to set them up
::
::
@@ -0,0 +1,2 @@
title: Networking
icon: i-lucide-network
@@ -1,27 +1,19 @@
--- ---
navigation: true
title: NAT & DHCP title: NAT & DHCP
description: Learn how NAT, port forwarding, and DHCP work on a home router. Configure fixed IP leases and understand how to expose local services. description: Learn how NAT, port forwarding, and DHCP work on a home router. Configure fixed IP leases and understand how to expose local services.
main:
fluid: false
--- ---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Router and NAT
::alert{type="info"} :ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
🎯 __Goals:__
- Understand how port forwarding works
- Learn how to configure router NAT
- Learn how to issue DHCP leases (fixed IPs)
::
![picture](/img/global/nat.svg) ![picture](/img/global/nat.svg)
## What is a "port"? ## What is a "port"?
---
Ports are different channels through which your router sends and receives data. This allows multiple services to run simultaneously. Ports are different channels through which your router sends and receives data. This allows multiple services to run simultaneously.
When it receives data through a port, your router forwards that data to the machine that: When it receives data through a port, your router forwards that data to the machine that:
- either initiated the request, - either initiated the request,
- or is configured to receive data on a specific port. - or is configured to receive data on a specific port.
@@ -32,7 +24,7 @@ Some programs and applications are designed to use specific ports. For example,
So, your router acts as a data dispatcher between the internet and your local machines. So, your router acts as a data dispatcher between the internet and your local machines.
## Port Forwarding ## Port Forwarding
---
Forwarding a `port` means setting a rule that specifies which `source` can send data to which `port` on your router, which will then forward it to a specific `port` on a specific `machine`. The `sources` and `destination machine` are identified by their IP addresses. Forwarding a `port` means setting a rule that specifies which `source` can send data to which `port` on your router, which will then forward it to a specific `port` on a specific `machine`. The `sources` and `destination machine` are identified by their IP addresses.
| Variable | Description | Example | | Variable | Description | Example |
@@ -51,18 +43,17 @@ This is useful when you have a server that must be accessible from the internet.
To make the website accessible, you'll configure your router to redirect the domain request to your local server. To make the website accessible, you'll configure your router to redirect the domain request to your local server.
Assume your service runs on port `3000` locally (`http://192.168.1.50:3000`), you would redirect all traffic from port `443` on the router to port `3000` on the local server. Assume your service runs on port `3000` locally (`http://192.168.1.50:3000`), you would redirect all traffic from port `443` on the router to port `3000` on the local server.
::alert{type="warning"} ::warning{to="/serveex/core/swag"}
:::list{type="warning"}
- __Warning:__ If you have multiple services to expose like `subdomain1.mydomain.com` and `subdomain2.mydomain.com`, your router cannot differentiate requests and forward to different ports. __Warning:__ If you have multiple services to expose like `subdomain1.mydomain.com` and `subdomain2.mydomain.com`, your router cannot differentiate requests and forward to different ports.
You must use a [Reverse Proxy](../../serveex/core/swag) to route traffic based on the request. You must use a **Reverse Proxy** to route traffic based on the request.
:::
:: ::
## DHCP ## DHCP
---
Every time a device connects to your local network, your router assigns it an IP address using DHCP rules. Every time a device connects to your local network, your router assigns it an IP address using DHCP rules.
This IP is randomly selected from a predefined pool. This IP is randomly selected from a predefined pool.
At every device reboot, the IP may change which is problematic if you're forwarding ports, as the target IP may no longer be valid. At every device reboot, the IP may change, which is problematic if you're forwarding ports, as the target IP may no longer be valid.
To avoid this, use your router's DHCP server to assign a static IP address. To avoid this, use your router's DHCP server to assign a static IP address.
@@ -1,21 +1,13 @@
--- ---
navigation: true
title: DNS Zone title: DNS Zone
description: Understand how DNS works, how to read and edit a DNS zone, and how to configure domain names for your self-hosted services. description: Understand how DNS works, how to read and edit a DNS zone, and how to configure domain names for your self-hosted services.
main:
fluid: false
--- ---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Domain Names and DNS Zones
::alert{type="info"}
🎯 __Objectives:__ :ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
- Understand how a DNS server works
- Learn how to edit a DNS zone
::
## Introduction ## Introduction
---
When you browse a website or use an app, requests are made to one or more domains to fetch content for the page. Your device doesn't know the IP addresses of these servers, so it contacts a _name server_ (Domain Name Server), which responds with the most up-to-date IP address for the domain being requested. When you browse a website or use an app, requests are made to one or more domains to fetch content for the page. Your device doesn't know the IP addresses of these servers, so it contacts a _name server_ (Domain Name Server), which responds with the most up-to-date IP address for the domain being requested.
The DNS zone is like a registry with signposts that direct your requests to the correct destination. The DNS zone is like a registry with signposts that direct your requests to the correct destination.
@@ -23,14 +15,13 @@ The DNS zone is like a registry with signposts that direct your requests to the
![Picture](/img/global/dns.svg) ![Picture](/img/global/dns.svg)
## The DNS Zone ## The DNS Zone
---
When you purchase a domain from a registrar (Cloudflare, OVH, etc.), the registrar assigns you a DNS zone that you can customize. When you purchase a domain from a registrar (Cloudflare, OVH, etc.), the registrar assigns you a DNS zone that you can customize.
You can enter _records_ into this DNS zone to direct requests properly. You can find [more information here](https://help.ovhcloud.com/csm/fr-dns-servers-general-information?id=kb_article_view&sysparm_article=KB0051661). You can enter _records_ into this DNS zone to direct requests properly. You can find [more information here](https://help.ovhcloud.com/csm/fr-dns-servers-general-information?id=kb_article_view&sysparm_article=KB0051661).
Example of a DNS zone for the domain `mydomain.com`: Example of a DNS zone for the domain `mydomain.com`:
``` ```
@ IN SOA ns1.dns.me. dns.net. (2024051800 86400 3600 3600000 60) @ IN SOA ns1.dns.me. dns.net. (2024051800 86400 3600 3600000 60)
IN NS ns1.dns.me. IN NS ns1.dns.me.
@@ -40,7 +31,6 @@ www IN CNAME mydomain.com
sousdomaine IN CNAME mydomain.com sousdomaine IN CNAME mydomain.com
``` ```
In this example: In this example:
- `$TTL 3600` tells global name servers that the records are valid for 1 hour (after which they need to re-check). - `$TTL 3600` tells global name servers that the records are valid for 1 hour (after which they need to re-check).
@@ -51,20 +41,18 @@ In this example:
So, if you want to point `mydomain.com` to your server, you can do it by adding an `A` record pointing to your server's public IP address. So, if you want to point `mydomain.com` to your server, you can do it by adding an `A` record pointing to your server's public IP address.
::alert{type="warning"} ::warning
:::list{type="warning"}
- __Warning:__ If your server is hosted at home: __Warning:__ If your server is hosted at home:
:::
- Your public IP is the one assigned to your home router. Make sure it's static, or configure [DDNS](https://aws.amazon.com/fr/what-is/dynamic-dns/). - Your public IP is the one assigned to your home router. Make sure it's static, or configure [DDNS](https://aws.amazon.com/fr/what-is/dynamic-dns/).
- Make sure you've [set up port 443 forwarding to your server's listening port](/general/networking/nat). - Make sure you've [set up port 443 forwarding to your server's listening port](/general/networking/nat).
:: ::
If you're adding a subdomain that should also point to your server, use a `CNAME` record pointing to `mydomain.com`. If you're adding a subdomain that should also point to your server, use a `CNAME` record pointing to `mydomain.com`.
::alert{type="info"} ::note
:::list{type="info"}
- __Why not use an `A` record for the subdomain?__ If your subdomain points to the same server as `mydomain.com`, it's better to use a `CNAME` record because if the server's IP changes, you wont need to update the subdomain record. __Why not use an `A` record for the subdomain?__ If your subdomain points to the same server as `mydomain.com`, it's better to use a `CNAME` record because if the server's IP changes, you wont need to update the subdomain record.
:::
:: ::
Most registrars offer user-friendly interfaces to manage DNS records. Refer to your registrars documentation for specific instructions. Most registrars offer user-friendly interfaces to manage DNS records. Refer to your registrars documentation for specific instructions.
@@ -1,43 +1,34 @@
--- ---
navigation: true
title: Samba title: Samba
description: Set up Samba on Debian to share folders over your local network and access them from Windows, macOS, or Linux. description: Set up Samba on Debian to share folders over your local network and access them from Windows, macOS, or Linux.
main:
fluid: false
--- ---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Samba
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
Samba is a protocol that allows access to a folder located on a network drive. It can be configured on macOS, Windows, or Linux. Samba is a protocol that allows access to a folder located on a network drive. It can be configured on macOS, Windows, or Linux.
There are many tutorials for setting up Samba on Windows or on NAS systems like Synology, but here we focus on Debian. There are many tutorials for setting up Samba on Windows or on NAS systems like Synology, but here we focus on Debian.
::alert{type="info"}
🎯 __Objectives:__
- Create a network folder on a remote machine
- Access the network folder from our server
::
![samba](/img/global/smb.svg) ![samba](/img/global/smb.svg)
## Sharing a Network Folder ## Create and configure a Shared Network Folder
--- ::note
::alert{type="info"}
:::list{type="info"} In this example, we will share the `/video` folder from a remote machine called `remote-machine`. We will access this folder from a machine called `local-machine`. The user connecting to the network drive will be `sambauser`.
- In this example, we will share the `/video` folder from a remote machine called `remote-machine`. We will access this folder from a machine called `local-machine`. The user connecting to the network drive will be `sambauser`.
:::
:: ::
::steps{level="3"}
### Install Samba Server ### Install Samba Server
```sh ```bash [Terminal]
sudo apt update && sudo apt upgrade sudo apt update && sudo apt upgrade
sudo apt install samba smbclient cifs-utils sudo apt install samba smbclient cifs-utils
``` ```
### Create the `/video` Folder ### Create the `/video` Folder
```sh ```bash [Terminal]
sudo mkdir /video sudo mkdir /video
``` ```
@@ -45,19 +36,19 @@ sudo mkdir /video
Now, edit the file `/etc/samba/smb.conf`. Now, edit the file `/etc/samba/smb.conf`.
::alert{type="success"} ```bash [Terminal]
__Tip:__ You can use [File Browser](/serveex/files/file-browser) to navigate and edit your files instead of using terminal commands. sudo nano /etc/samba/smb.conf
::
```sh
sudo vim /etc/samba/smb.conf
``` ```
Find the `workgroup` variable, press `i` to enter insert mode, and name your workgroup (e.g., `workgroup = WORKGROUP`). ::tip{icon="" to="/serveex/files/file-browser-quantum"}
__Tip:__ You can use **File Browser Quantum** to navigate and edit your files instead of using terminal commands.
::
Find the `workgroup` variable and name your workgroup (e.g., `workgroup = WORKGROUP`).
Then scroll to the end of the file and add the following configuration: Then scroll to the end of the file and add the following configuration:
```properties ```properties [smb.conf]
[video] [video]
comment = Video folder comment = Video folder
path = /video path = /video
@@ -69,77 +60,75 @@ Then scroll to the end of the file and add the following configuration:
inherit permissions = yes inherit permissions = yes
``` ```
Press `Esc` to exit insert mode, then type `:x` and press `Enter` to save and exit. Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
### Done !
::
### Create a Samba User and Group ### Create a Samba User and Group
Since we're using a secured share, we need to create a user and group to access it remotely. Since we're using a secured share, we need to create a user and group to access it remotely.
Create the group: ::steps{level="3"}
### Create the group
```sh ```bash [Terminal]
sudo groupadd smbshare sudo groupadd smbshare
``` ```
Give the group control over the `/video` folder: Give the group control over the `/video` folder:
```sh ```bash [Terminal]
sudo chgrp -R smbshare /video sudo chgrp -R smbshare /video
``` ```
Set inherited permissions: Set inherited permissions:
```sh ```bash [Terminal]
sudo chmod 2775 /video sudo chmod 2775 /video
``` ```
### Create the user
Now add a no-login user this user cannot log into the server but can access Samba. Now add a no-login user: this user cannot log into the server but can access Samba.
```sh ```bash [Terminal]
sudo useradd -M -s /sbin/nologin sambauser sudo useradd -M -s /sbin/nologin sambauser
``` ```
Add the user to the `smbshare` group: Add the user to the `smbshare` group:
```sh ```bash [Terminal]
sudo usermod -aG smbshare sambauser sudo usermod -aG smbshare sambauser
``` ```
Set a Samba password: Set a Samba password:
```sh ```bash [Terminal]
sudo smbpasswd -a sambauser sudo smbpasswd -a sambauser
``` ```
Enable the Samba account: ### Enable the Samba account
```sh ```bash [Terminal]
sudo smbpasswd -e sambauser sudo smbpasswd -e sambauser
``` ```
### Done !
```sh
sudo ufw allow from remote-ip to any app Samba
:: ::
```
## Accessing a Shared Folder ## Accessing a Shared Folder
--- ::steps{level="3"}
\::
### Install Required Packages ### Install Required Packages
```sh ```bash [Terminal]
sudo apt update && sudo apt upgrade sudo apt update && sudo apt upgrade
sudo apt install cifs-utils sudo apt install cifs-utils
``` ```
### Create the Mount Destination ### Create the Mount Destination
We will create a folder on our local machine where the remote `/video` folder will be mounted e.g., `/mnt/video`. We will create a folder on our local machine where the remote `/video` folder will be mounted, e.g. `/mnt/video`.
```sh ```bash [Terminal]
sudo mkdir /mnt/video sudo mkdir /mnt/video
``` ```
@@ -149,14 +138,14 @@ To avoid typing our username and password every time, create a `.credentials` fi
Create it in the `/smb` folder: Create it in the `/smb` folder:
```sh ```bash [Terminal]
sudo mkdir /smb sudo mkdir /smb
sudo vi /smb/.credentials sudo nano /smb/.credentials
``` ```
Enter insert mode (`i`) and write: Write:
```properties ```properties [.credentials]
username=smbuser username=smbuser
password=password password=password
``` ```
@@ -164,19 +153,24 @@ password=password
* `smbuser`: the user we created on the `remote-machine` * `smbuser`: the user we created on the `remote-machine`
* `password`: the password set earlier * `password`: the password set earlier
Press `Esc`, then `:x` and `Enter` to save and exit. Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
Set proper file permissions: Set proper file permissions:
```sh ```bash [Terminal]
sudo chmod 600 /smb/.credentials sudo chmod 600 /smb/.credentials
``` ```
### Mount the Shared Folder ### Mount the Shared Folder
::warning
__Warning:__ If you're using ufw as firewall, you need to add a rule to allow your remote server to access to your share.
```bash [Terminal]
sudo ufw allow from <your-remote-ip> to any app Samba
```
::
Now mount the folder: Now mount the folder:
```sh ```bash [Terminal]
sudo mount -t cifs -o credentials=/smb/.credentials //remote-ip/video /mnt/video sudo mount -t cifs -o credentials=/smb/.credentials //remote-ip/video /mnt/video
``` ```
@@ -184,7 +178,7 @@ Replace `remote-ip` with your `remote-machine`'s IP address.
Verify the mount: Verify the mount:
```sh ```bash [Terminal]
sudo mount -t cifs sudo mount -t cifs
``` ```
@@ -192,38 +186,40 @@ Youll see details confirming the mount is successful.
Now you can access the `/video` folder of the `remote-machine` from your `local-machine`! Now you can access the `/video` folder of the `remote-machine` from your `local-machine`!
### Auto-mount on Boot ### (Optional) Auto-mount on Boot
By default, shares aren't auto-mounted after reboot. To automate this, edit the `/etc/fstab` file. By default, shares aren't auto-mounted after reboot. To automate this, edit the `/etc/fstab` file.
First, back it up: First, back it up:
```sh ```bash [Terminal]
sudo cp /etc/fstab /etc/fstab.bak sudo cp /etc/fstab /etc/fstab.bak
``` ```
Then add the mount configuration line: Then add the mount configuration line:
```sh ```bash [Terminal]
sudo echo //remote-ip/video /mnt/video cifs _netdev,nofail,credentials=/smb/.credentials,x-systemd.automount,x-systemd.device-timeout=15 0 0 >> /etc/fstab sudo echo //remote-ip/video /mnt/video cifs _netdev,nofail,credentials=/smb/.credentials,x-systemd.automount,x-systemd.device-timeout=15 0 0 >> /etc/fstab
``` ```
Reboot the machine: Reboot the machine:
```sh ```bash [Terminal]
sudo reboot sudo reboot
``` ```
After rebooting, verify the mount: After rebooting, verify the mount:
```sh ```bash [Terminal]
sudo mount -t cifs sudo mount -t cifs
``` ```
### And done!
::
And done! ::tip
__Unmount the Shared Folder__
### Unmount the Shared Folder ```bash [Terminal]
```sh
sudo umount -t cifs /mnt/video sudo umount -t cifs /mnt/video
``` ```
::
@@ -0,0 +1,2 @@
title: Storage
icon: i-lucide-hard-drive
@@ -1,12 +1,10 @@
--- ---
navigation: true
title: RAID title: RAID
description: Understand RAID concepts hardware vs software, RAID levels, and how to set up redundant disk arrays for your homelab. description: Understand RAID concepts, hardware vs software, RAID levels, and how to set up redundant disk arrays for your homelab.
main:
fluid: false
--- ---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# RAID
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
_Redundant Array of Independent Disks_ _Redundant Array of Independent Disks_
@@ -24,13 +22,12 @@ There are several types of RAID, each offering its own pros and cons. In general
- Write speed - Write speed
- Fault tolerance (resistance to hardware failure) - Fault tolerance (resistance to hardware failure)
::alert{type="warning"} ::warning
:::list{type="warning"}
- RAID is not a backup system but a service continuity system! It only allows hot-swapping of drives without interrupting your server or restoring from backup. You still need an external backup system. RAID is not a backup system but a service continuity system! It only allows hot-swapping of drives without interrupting your server or restoring from backup. You still need an external backup system.
:: ::
### No RAID ### No RAID
---
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<img src="/img/global/no-raid.svg" alt="Image" style="max-width: 30%; max-height:230px; margin-right: 20px;"> <img src="/img/global/no-raid.svg" alt="Image" style="max-width: 30%; max-height:230px; margin-right: 20px;">
@@ -38,12 +35,12 @@ There are several types of RAID, each offering its own pros and cons. In general
<li>Just your disks, without RAID. Data is stored disk by disk.</li> <li>Just your disks, without RAID. Data is stored disk by disk.</li>
<li>If you lose a disk, only its data is lost.</li> <li>If you lose a disk, only its data is lost.</li>
<li>Total capacity is the sum of all disks.</li> <li>Total capacity is the sum of all disks.</li>
</ul>
</div> </div>
Use your disks without RAID when you're not afraid of data loss and can tolerate service interruptions between failure and backup restoration. Use your disks without RAID when you're not afraid of data loss and can tolerate service interruptions between failure and backup restoration.
### RAID 0 ### RAID 0
---
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<img src="/img/global/raid0.svg" alt="Image" style="max-width: 30%; max-height:230px; margin-right: 20px;"> <img src="/img/global/raid0.svg" alt="Image" style="max-width: 30%; max-height:230px; margin-right: 20px;">
@@ -54,12 +51,12 @@ Use your disks without RAID when you're not afraid of data loss and can tolerate
<li>High read and write performance (multiplied by number of disks).</li> <li>High read and write performance (multiplied by number of disks).</li>
<li>Total capacity is the sum of all disks.</li> <li>Total capacity is the sum of all disks.</li>
<li>Minimum of 2 disks required.</li> <li>Minimum of 2 disks required.</li>
</ul>
</div> </div>
Use RAID 0 when you prioritize performance and are not concerned about data loss. Ideal for temporary, high-speed storage (video editing, AI workloads, etc). Not suitable for long-term storage, as one failure means total data loss. Use RAID 0 when you prioritize performance and are not concerned about data loss. Ideal for temporary, high-speed storage (video editing, AI workloads, etc). Not suitable for long-term storage, as one failure means total data loss.
### RAID 1 ### RAID 1
---
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<img src="/img/global/raid1.svg" alt="Image" style="max-width: 30%; max-height:230px; margin-right: 20px;"> <img src="/img/global/raid1.svg" alt="Image" style="max-width: 30%; max-height:230px; margin-right: 20px;">
@@ -70,16 +67,16 @@ Use RAID 0 when you prioritize performance and are not concerned about data loss
<li>Improved read speed (scales with number of disks).</li> <li>Improved read speed (scales with number of disks).</li>
<li>Total capacity is equal to one disk (e.g., 2×10TB = 10TB).</li> <li>Total capacity is equal to one disk (e.g., 2×10TB = 10TB).</li>
<li>Minimum of 2 disks required.</li> <li>Minimum of 2 disks required.</li>
</ul>
</div> </div>
Use RAID 1 for strong redundancy. Each disk contains all data, so performance remains unaffected during a failure. Once failed disks are replaced, data is quickly restored. However, usable storage is limited to one disks capacity, making it an expensive solution. Use RAID 1 for strong redundancy. Each disk contains all data, so performance remains unaffected during a failure. Once failed disks are replaced, data is quickly restored. However, usable storage is limited to one disks capacity, making it an expensive solution.
::alert{type="success"} ::tip{icon=""}
__Tip:__ You can combine RAID 1 with other RAID types to create mirrored arrays. __Tip:__ You can combine RAID 1 with other RAID types to create mirrored arrays.
:: ::
### RAID 5 ### RAID 5
---
<p align="center"> <p align="center">
<img src="/img/global/raid5.svg" alt="Image" style="max-width: 40%; margin-right: 20px;"> <img src="/img/global/raid5.svg" alt="Image" style="max-width: 40%; margin-right: 20px;">
</p> </p>
@@ -94,7 +91,6 @@ Use RAID 1 for strong redundancy. Each disk contains all data, so performance re
Use RAID 5 when you want reliable storage with 3 to 5 disks and minimal space loss. It tolerates one disk failure but may have degraded performance during recovery, which can take days. Use RAID 5 when you want reliable storage with 3 to 5 disks and minimal space loss. It tolerates one disk failure but may have degraded performance during recovery, which can take days.
### RAID 6 ### RAID 6
---
<p align="center"> <p align="center">
<img src="/img/global/raid6.svg" alt="Image" style="max-width: 50%; margin-right: 20px;"> <img src="/img/global/raid6.svg" alt="Image" style="max-width: 50%; margin-right: 20px;">
</p> </p>
@@ -1,26 +1,22 @@
--- ---
navigation: true
title: ZFS title: ZFS
description: Introduction to ZFS a combined file system and volume manager with snapshots, checksums, and built-in redundancy for reliable homelab storage. description: Introduction to ZFS, a combined file system and volume manager with snapshots, checksums, and built-in redundancy for reliable homelab storage.
main:
fluid: false
--- ---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# ZFS
::alert{type="info"}
🎯 __Objectives:__ :ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
- Understand what ZFS is and why it's useful
::
ZFS is widely used in the world of servers, NAS systems (like FreeNAS / TrueNAS), virtualization, and even by tech-savvy individuals who want reliable storage. It is both a _file system_ (like NTFS for Windows, EXT4, FAT32, etc.) and a _volume manager_ (similar to LVM). ZFS is widely used in the world of servers, NAS systems (like FreeNAS / TrueNAS), virtualization, and even by tech-savvy individuals who want reliable storage. It is both a _file system_ (like NTFS for Windows, EXT4, FAT32, etc.) and a _volume manager_ (similar to LVM).
To put it simply: To put it simply:
- A **volume manager** organizes physical storage (like one or more hard drives). - A **volume manager** organizes physical storage (like one or more hard drives).
- A **file system** organizes how data blocks are written, read, and deleted within those volumes. - A **file system** organizes how data blocks are written, read, and deleted within those volumes.
ZFS goes far beyond traditional file systems in terms of performance and features. ZFS goes far beyond traditional file systems in terms of performance and features.
Heres what were most interested in: Heres what were most interested in:
- Its __snapshot management__ features, allowing you to quickly roll back in case of issues. - Its __snapshot management__ features, allowing you to quickly roll back in case of issues.
- Its support for disk groupings and [__RAID-like structures__](/general/storage/raid) (Z-Mirror, RAIDZ1, RAIDZ2, RAIDZ3). - Its support for disk groupings and [__RAID-like structures__](/general/storage/raid) (Z-Mirror, RAIDZ1, RAIDZ2, RAIDZ3).
- Its __automatic recovery of corrupted data__ (through scrubbing). - Its __automatic recovery of corrupted data__ (through scrubbing).
@@ -28,7 +24,6 @@ Heres what were most interested in:
- Its robust error notifications and monitoring. - Its robust error notifications and monitoring.
## Structure ## Structure
---
![](/img/global/zfs.svg) ![](/img/global/zfs.svg)
ZFS has a unique structure: ZFS has a unique structure:
@@ -38,35 +33,35 @@ ZFS has a unique structure:
- **dataset**: a logical data container within a zpool. Each dataset can have its own settings (compression, quotas, permissions, etc.). - **dataset**: a logical data container within a zpool. Each dataset can have its own settings (compression, quotas, permissions, etc.).
There are several dataset types: There are several dataset types:
- **file system**: a standard ZFS filesystem, mounted without storage quotas. - **file system**: a standard ZFS filesystem, mounted without storage quotas.
- **zvol**: a "virtual disk" with a defined size, which you can format and partition as if it were a physical disk. - **zvol**: a "virtual disk" with a defined size, which you can format and partition as if it were a physical disk.
- **snapshot**: a frozen-in-time version of another dataset. Snapshots can be created manually or through backup tools. They can be mounted to browse data as it was at the snapshot time. - **snapshot**: a frozen-in-time version of another dataset. Snapshots can be created manually or through backup tools. They can be mounted to browse data as it was at the snapshot time.
## Why ZFS over others? ## Why ZFS over others?
---
### Data Integrity ### Data Integrity
ZFS continuously checks that your stored data hasn't become corrupted. Every block of data is associated with a checksum, allowing ZFS to detect even the smallest alteration. If corruption is found and a healthy copy exists elsewhere, ZFS can repair the data automatically. ZFS continuously checks that your stored data hasn't become corrupted. Every block of data is associated with a checksum, allowing ZFS to detect even the smallest alteration. If corruption is found and a healthy copy exists elsewhere, ZFS can repair the data automatically.
### Built-in RAID ### Built-in RAID
ZFS includes its own volume management system (vdevs). You can build a zpool using multiple diskssimilar to traditional [RAID](/general/storage/raid) setupsbut with more flexibility. For example: ZFS includes its own volume management system (vdevs). You can build a zpool using multiple disks, similar to traditional [RAID](/general/storage/raid) setups, but with more flexibility. For example:
- **Z-mirror** → equivalent to RAID 1 - **Z-mirror** → equivalent to RAID 1
- **RAIDZ1** → equivalent to RAID 5 (tolerates 1 disk failure) - **RAIDZ1** → equivalent to RAID 5 (tolerates 1 disk failure)
- **RAIDZ2** → equivalent to RAID 6 (tolerates 2 disk failures) - **RAIDZ2** → equivalent to RAID 6 (tolerates 2 disk failures)
- **RAIDZ3** → tolerates up to 3 disk failures - **RAIDZ3** → tolerates up to 3 disk failures
ZFS handles all this nativelyno external RAID software needed. ZFS handles all this natively: no external RAID software needed.
::alert{type="info"} ::note{to="/general/storage/raid"}
:::list{type="info"}
- Check out the [article on RAID](/general/storage/raid) to find the right solution for your needs. Check out the **article on RAID** to find the right solution for your needs.
:::
:: ::
### Snapshots and Clones ### Snapshots and Clones
ZFS allows you to create snapshotsinstantaneous images of a dataset's state. Snapshots take up minimal space and can be scheduled frequently. You can also create clones: writable copies of snapshots. ZFS allows you to create snapshots: instantaneous images of a dataset's state. Snapshots take up minimal space and can be scheduled frequently. You can also create clones: writable copies of snapshots.
### Compression and Deduplication ### Compression and Deduplication
@@ -0,0 +1,2 @@
title: Hardware
icon: i-lucide-server
@@ -1,34 +1,27 @@
--- ---
navigation: true
title: The Basics title: The Basics
description: Overview of server hardware fundamentals CPUs, RAM, storage, and form factors to understand before building your homelab. description: Overview of server hardware fundamentals. CPUs, RAM, storage, and form factors to understand before building your homelab.
main:
fluid: false
--- ---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Server Basics
::alert{type="info"}
🎯 __Objectives:__ :ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
- Understand the fundamentals of server hardware
::
![hardware](/img/global/hardware.svg) ![hardware](/img/global/hardware.svg)
A __server__ is essentially a computer dedicated to specific tasks, designed to remain accessible at all times. Structurally, it's not much different from a regular computer. Depending on its intended use, some components may vary. This article serves as a reference to help you understand the essential components of a server and how their roles adapt based on your needs. A __server__ is essentially a computer dedicated to specific tasks, designed to remain accessible at all times. Structurally, it's not much different from a regular computer. Depending on its intended use, some components may vary. This article serves as a reference to help you understand the essential components of a server and how their roles adapt based on your needs.
## Motherboard ## Motherboard
---
The __motherboard__ is the foundation of your machine. It's the component that connects all others together. It enables communication between components and interaction with peripherals (keyboard, mouse, etc.). Choose it based on your I/O (Input/Output) needs like USB ports, network ports, speed, etc., and ensure compatibility with the components you plan to install. The __motherboard__ is the foundation of your machine. It's the component that connects all others together. It enables communication between components and interaction with peripherals (keyboard, mouse, etc.). Choose it based on your I/O (Input/Output) needs like USB ports, network ports, speed, etc., and ensure compatibility with the components you plan to install.
Key components connected to the motherboard: Key components connected to the motherboard:
- CPU - CPU
- RAM - RAM
- Storage (HDD and/or SSD) - Storage (HDD and/or SSD)
- Optional dedicated GPU - Optional dedicated GPU
Common consumer motherboard formats: Common consumer motherboard formats:
- E-ATX: largest - E-ATX: largest
- ATX: standard - ATX: standard
- Micro-ATX: smaller - Micro-ATX: smaller
@@ -37,27 +30,25 @@ Common consumer motherboard formats:
Larger boards generally offer more ports and features. Pre-built systems might use proprietary formats. Larger boards generally offer more ports and features. Pre-built systems might use proprietary formats.
## CPU ## CPU
---
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<img src="/img/global/cpu.svg" alt="Image" style="max-width: 25%; max-height:230px; margin-right: 20px;"> <img src="/img/global/cpu.svg" alt="Image" style="max-width: 25%; max-height:230px; margin-right: 20px;">
<p>The <strong>CPU</strong> (Central Processing Unit) is the computer's calculator. It processes most software tasks. Modern CPUs have multiple cores, often with virtual threads, to better handle workloads. They need to be cooled using either an active cooler (with a fan) or a passive one (fanless), depending on power consumption (watts). Choose your CPU based on how you plan to use the server.</p> <p>The <strong>CPU</strong> (Central Processing Unit) is the computer's calculator. It processes most software tasks. Modern CPUs have multiple cores, often with virtual threads, to better handle workloads. They need to be cooled using either an active cooler (with a fan) or a passive one (fanless), depending on power consumption (watts). Choose your CPU based on how you plan to use the server.</p>
</div> </div>
::alert{type="warning"} ::warning
:::list{type="warning"}
- __Caution:__ Ensure third-party coolers are compatible with the CPU socket and always apply thermal paste before installing the cooler. __Caution:__ Ensure third-party coolers are compatible with the CPU socket and always apply thermal paste before installing the cooler.
:::
:: ::
Consider: Consider:
- Number of cores (more cores = better multitasking) - Number of cores (more cores = better multitasking)
- Clock speed in GHz - Clock speed in GHz
- Power consumption in Watts - Power consumption in Watts
For low-power home servers or NAS (non-intensive computing), consider Intel N100/150 (4 cores) or N305/N355 (8 cores)efficient and low power (ideal for 24/7 uptime). For low-power home servers or NAS (non-intensive computing), consider Intel N100/150 (4 cores) or N305/N355 (8 cores), efficient and low power (ideal for 24/7 uptime).
## RAM ## RAM
---
<p align="center"> <p align="center">
<img src="/img/global/ram.svg" alt="Image" style="max-width: 65%;"> <img src="/img/global/ram.svg" alt="Image" style="max-width: 65%;">
@@ -68,33 +59,30 @@ __RAM__ (Random Access Memory) is fast, temporary memory used by the CPU (and iG
Comes as sticks installed on the motherboard. Varies by format and generation (currently DDR5). Comes as sticks installed on the motherboard. Varies by format and generation (currently DDR5).
## GPU ## GPU
---
The __GPU__ (Graphics Processing Unit) handles graphical, video, and sometimes AI-related processing. Its main theoretical use is to display the image on your screen. In servers, it's useful for media centers (e.g. [Plex](/serveex/media/plex)) and for accelerating AI tasks like facial recognition or photo indexing (e.g. [Immich](/serveex/cloud/immich)). The __GPU__ (Graphics Processing Unit) handles graphical, video, and sometimes AI-related processing. Its main theoretical use is to display the image on your screen. In servers, it's useful for media centers (e.g. [Jellyfin](/serveex/media/jellyfin)) and for accelerating AI tasks like facial recognition or photo indexing (e.g. [Immich](/serveex/cloud/immich)).
Depending on the required performance, one can choose between a dedicated GPU with its own VRAM (a graphics card connected to a PCIe slot on the motherboard), or an iGPUan integrated GPU built into the CPU (such as the N100/N150 or N305/N355), which uses the systems shared RAM. Depending on the required performance, one can choose between a dedicated GPU with its own VRAM (a graphics card connected to a PCIe slot on the motherboard), or an iGPU, an integrated GPU built into the CPU (such as the N100/N150 or N305/N355), which uses the systems shared RAM.
### HDD(s) ### HDD(s)
---
<p align="center"> <p align="center">
<img src="/img/global/hdd.svg" alt="Image" style="max-width: 50%; margin-right: 20px;"> <img src="/img/global/hdd.svg" alt="Image" style="max-width: 50%; margin-right: 20px;">
</p> </p>
An __HDD__ (Hard Disk Drive), or hard drive, is a component used to store data. It was once the standard storage device in computers. HDDs consist of one or more stacked platters and read/write headssomewhat like a vinyl record player. An __HDD__ (Hard Disk Drive), or hard drive, is a component used to store data. It was once the standard storage device in computers. HDDs consist of one or more stacked platters and read/write heads, somewhat like a vinyl record player.
Today, HDDs can store enormous amounts of data (up to 30TB, or 30,000 gigabytes, for consumer models), but their read and write speeds are limited due to their mechanical nature. They are also bulky and heavy. Today, HDDs can store enormous amounts of data (up to 30TB, or 30,000 gigabytes, for consumer models), but their read and write speeds are limited due to their mechanical nature. They are also bulky and heavy.
Generally, HDDs are best suited for storing data that doesnt require frequent access or fast write speeds, such as media files (videos, photos), cloud drives, or archived data. They perform well in these scenarios and, most importantly, are significantly cheaper than SSDs for the same amount of storage. Generally, HDDs are best suited for storing data that doesnt require frequent access or fast write speeds, such as media files (videos, photos), cloud drives, or archived data. They perform well in these scenarios and, most importantly, are significantly cheaper than SSDs for the same amount of storage.
::alert{type="success"} ::tip{icon="" to="/general/storage/raid"}
__Tip:__ Use multiple HDDs in [RAID](/general/storage/raid) to enhance performance and redundancy. __Tip:__ Use multiple HDDs in **RAID** to enhance performance and redundancy.
:: ::
Comes in 3.5" and 2.5" formats; servers usually favor the more reliable 3.5". Comes in 3.5" and 2.5" formats; servers usually favor the more reliable 3.5".
### SSD(s) ### SSD(s)
---
<p align="center"> <p align="center">
<img src="/img/global/nvme.svg" alt="Image" style="max-width: 50%; margin-right: 20px;"> <img src="/img/global/nvme.svg" alt="Image" style="max-width: 50%; margin-right: 20px;">
@@ -102,16 +90,15 @@ Comes in 3.5" and 2.5" formats; servers usually favor the more reliable 3.5".
An __SSD__ (Solid State Drive) is a small circuit board with memory chips soldered onto it, used to store information. Unlike RAM, these chips retain data even when not powered, meaning the information is preserved after a reboot. SSDs are generally used as the main storage medium for your server. An __SSD__ (Solid State Drive) is a small circuit board with memory chips soldered onto it, used to store information. Unlike RAM, these chips retain data even when not powered, meaning the information is preserved after a reboot. SSDs are generally used as the main storage medium for your server.
Unlike HDDs, SSDs have no moving parts, are highly compact, and most importantly, are extremely fastoffering speeds of several gigabytes per second for high-performance models. Unlike HDDs, SSDs have no moving parts, are highly compact, and most importantly, are extremely fast, offering speeds of several gigabytes per second for high-performance models.
SSDs come in various formats, but today the preferred choice is the M.2 NVMe version, as it is the smallest, fastest, and has become the standard on modern motherboards. SSDs come in various formats, but today the preferred choice is the M.2 NVMe version, as it is the smallest, fastest, and has become the standard on modern motherboards.
However, SSDs are significantly more expensive than hard drives for the same storage capacity. Typically, the operating system (OS) is installed on the SSD to ensure fast performance. In a server environment, it's also ideal to store [Docker containers](/serveex/core/docker) and databases on the SSD. More broadly, any data that needs to be accessed frequently and quicklysuch as websites, applications, or processing workloadsshould be stored on an SSD. However, SSDs are significantly more expensive than hard drives for the same storage capacity. Typically, the operating system (OS) is installed on the SSD to ensure fast performance. In a server environment, it's also ideal to store [Docker containers](/serveex/core/docker) and databases on the SSD. More broadly, any data that needs to be accessed frequently and quickly, such as websites, applications, or processing workloads, should be stored on an SSD.
### Network Card ### Network Card
---
A __network card__ allows your machine to communicate with your network (including the internet). It consists of a controller chip and one or more network ports. These portsoften Ethernet portscan come in different physical formats and support various data transfer standards: A __network card__ allows your machine to communicate with your network (including the internet). It consists of a controller chip and one or more network ports. These ports, often Ethernet ports, can come in different physical formats and support various data transfer standards:
- __RJ45 Gigabit Ethernet (10/100/1000):__ The standard RJ45 connector, supporting speeds from 10 Mbps (0.125 MB/s) up to 1000 Mbps (125 MB/s). - __RJ45 Gigabit Ethernet (10/100/1000):__ The standard RJ45 connector, supporting speeds from 10 Mbps (0.125 MB/s) up to 1000 Mbps (125 MB/s).
- __RJ45 2.5G:__ Same connector type, supporting up to 2.5 Gbps (2,500 Mbps or 312.5 MB/s). - __RJ45 2.5G:__ Same connector type, supporting up to 2.5 Gbps (2,500 Mbps or 312.5 MB/s).
@@ -120,10 +107,9 @@ A __network card__ allows your machine to communicate with your network (includi
- __SFP 1G:__ SFP port, commonly used for fiber optic connections, supporting speeds up to 1 Gbps. - __SFP 1G:__ SFP port, commonly used for fiber optic connections, supporting speeds up to 1 Gbps.
- __SFP+ 10G:__ An enhanced version of the SFP port, also used for fiber optics, supporting up to 10 Gbps. - __SFP+ 10G:__ An enhanced version of the SFP port, also used for fiber optics, supporting up to 10 Gbps.
::alert{type="warning"} ::warning
:::list{type="warning"}
- __Caution:__ Match network gear (router, switch, cables) to your desired speed. For most uses, CAT5E cables are enough; use CAT6A beyond 10 Gbps. Fiber requires additional care (simplex, duplex, transceivers...). __Caution:__ Match network gear (router, switch, cables) to your desired speed. For most uses, CAT5E cables are enough; use CAT6A beyond 10 Gbps. Fiber requires additional care (simplex, duplex, transceivers...).
:::
:: ::
The network card is usually built directly into the motherboard, but you can also use dedicated network cards, for example via USB or a PCIe expansion slot. The network card is usually built directly into the motherboard, but you can also use dedicated network cards, for example via USB or a PCIe expansion slot.
@@ -131,9 +117,9 @@ The network card is usually built directly into the motherboard, but you can als
In general, for a server setup, it's recommended to have at least two Ethernet ports to ensure redundancy in case one connection fails. In general, for a server setup, it's recommended to have at least two Ethernet ports to ensure redundancy in case one connection fails.
### Input/Output Ports ### Input/Output Ports
---
__I/O__ ports allow communication with external devices (displays, keyboard, mouse, network...). Motherboards typically offer: __I/O__ ports allow communication with external devices (displays, keyboard, mouse, network...). Motherboards typically offer:
- Ethernet ports - Ethernet ports
- USB ports (varied types/speeds) - USB ports (varied types/speeds)
- Video ports - Video ports
@@ -142,7 +128,6 @@ __I/O__ ports allow communication with external devices (displays, keyboard, mou
Choose a motherboard and expansions based on your I/O needs. Choose a motherboard and expansions based on your I/O needs.
### Power Supply ### Power Supply
---
The __power supply unit__ (PSU) is the component that provides electrical power to your machines components. It connects to the wall via a power cord and has several output cables that plug into the motherboard and various peripherals, such as hard drives or dedicated graphics cards. The __power supply unit__ (PSU) is the component that provides electrical power to your machines components. It connects to the wall via a power cord and has several output cables that plug into the motherboard and various peripherals, such as hard drives or dedicated graphics cards.
@@ -157,7 +142,6 @@ Another important factor is the form factor. There are several standard sizes, f
To choose the right PSU, a common rule of thumb is to estimate your systems power needs based on usage, and then double that value. This is because most power supplies operate at optimal efficiency around 50% of their maximum load. To choose the right PSU, a common rule of thumb is to estimate your systems power needs based on usage, and then double that value. This is because most power supplies operate at optimal efficiency around 50% of their maximum load.
### Case ### Case
---
<div style="display: flex; align-items: center;"> <div style="display: flex; align-items: center;">
<img src="/img/global/case.svg" alt="Image" style="max-width: 25%; max-height:230px; margin-right: 20px;"> <img src="/img/global/case.svg" alt="Image" style="max-width: 25%; max-height:230px; margin-right: 20px;">
@@ -1,61 +1,52 @@
--- ---
navigation: true
title: Network title: Network
description: Overview of networking hardware for homelabs — switches, NICs, cables, and how to connect your servers efficiently. description: Overview of networking hardware for homelabs. Switches, NICs, cables, and how to connect your servers efficiently.
main:
fluid: false
--- ---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Network
::alert{type="info"}
🎯 __Objectives:__ :ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
- Understand the basics of networking hardware
::
![hardware](/img/global/hardware-networking.svg) ![hardware](/img/global/hardware-networking.svg)
A computer network cannot exist without the hardware required to build it. Hardware determines the size of the network, communication speeds, and its overall performance. In this article, we will focus on the simplest types of networks, typically found in home environments. A computer network cannot exist without the hardware required to build it. Hardware determines the size of the network, communication speeds, and its overall performance. In this article, we will focus on the simplest types of networks, typically found in home environments.
## The Router ## The Router
--- The __router__ is the central hub of your network. It directs __packets__ (the blocks of data that travel across your network) from the sender to the appropriate recipient. It manages the routing of data both within your local network and to/from external networks. In short, it enables devices to communicate with each other and with the internet.
The __router__ is the central hub of your network. It directs __packets__—the blocks of data that travel across your network—from the sender to the appropriate recipient. It manages the routing of data both within your local network and to/from external networks. In short, it enables devices to communicate with each other and with the internet.
Everyone has a router at homeit's the __internet box__ provided by your ISP (Internet Service Provider). Everyone has a router at home: it's the __internet box__ provided by your ISP (Internet Service Provider).
In general, a router consists of: In general, a router consists of:
- a WAN (Wide Area Network) port that receives data from the internet (or from a higher-level network). For example, it could be a port for a fiber optic connection from your ISP, or an SFP+/RJ45 port for a third-party router. - a WAN (Wide Area Network) port that receives data from the internet (or from a higher-level network). For example, it could be a port for a fiber optic connection from your ISP, or an SFP+/RJ45 port for a third-party router.
- a switch, i.e., a hub with several __LAN__ (Local Area Network) ports allowing multiple devices to connect to your network. These ports can be RJ45 or SFP/SFP+. - a switch, i.e., a hub with several __LAN__ (Local Area Network) ports allowing multiple devices to connect to your network. These ports can be RJ45 or SFP/SFP+.
- sometimes a built-in WiFi transmitter/receiver. - sometimes a built-in WiFi transmitter/receiver.
A router may also include _firewall_ capabilities, allowing you to restrict traffic from specific devices, as well as _[NAT (Network Address Translation)](/general/networking/nat)_ for port forwarding. It generally includes a _[DHCP (Dynamic Host Configuration Protocol)](/general/networking/nat#dhcp)_ server to automatically assign _IP addresses_ to devices connected to the network. A router may also include _firewall_ capabilities, allowing you to restrict traffic from specific devices, as well as _[NAT (Network Address Translation)](/general/networking/nat)_ for port forwarding. It generally includes a _[DHCP (Dynamic Host Configuration Protocol)](/general/networking/nat#dhcp)_ server to automatically assign _IP addresses_ to devices connected to the network.
The router directly affects communication speeds between devices. The WAN port limits the maximum internet speed you can receive from your ISP. For example, if your subscription offers 5 Gb/s, youll need a WAN port that supports at least 5 Gb/s. Likewise, internal device-to-device communication is limited by the speed of the switch. If your devices communicate at 5 Gb/s, the routers switch must have 5 Gb/s ports. If you're using WiFi 7 equipment and want to enjoy its full speed, your router must support it as well. If youre using a separate WiFi access point, make sure its network port matches or exceeds the speed of the WiFi it broadcastsand that the router supports it too. The router directly affects communication speeds between devices. The WAN port limits the maximum internet speed you can receive from your ISP. For example, if your subscription offers 5 Gb/s, youll need a WAN port that supports at least 5 Gb/s. Likewise, internal device-to-device communication is limited by the speed of the switch. If your devices communicate at 5 Gb/s, the routers switch must have 5 Gb/s ports. If you're using WiFi 7 equipment and want to enjoy its full speed, your router must support it as well. If youre using a separate WiFi access point, make sure its network port matches or exceeds the speed of the WiFi it broadcasts, and that the router supports it too.
Internet speed, number of devices, WiFi speed, and internal network speedthese are the four key factors to consider when choosing an internet box or buying your own router. Internet speed, number of devices, WiFi speed, and internal network speed: these are the four key factors to consider when choosing an internet box or buying your own router.
::alert{type="success"} ::tip{icon=""}
✨ __Tip:__ ✨ __Tip:__
You can easily use a third-party router to manage your network if your ISPs internet box supports _bridge mode_. In France, only the provider Free offers this option. It is technically possible with other providers that do not support bridge mode, but it can be quite difficult and may prevent you from using all the features a third-party router provides. You can easily use a third-party router to manage your network if your ISPs internet box supports _bridge mode_. In France, only the provider Free offers this option. It is technically possible with other providers that do not support bridge mode, but it can be quite difficult and may prevent you from using all the features a third-party router provides.
:: ::
## The Switch ## The Switch
---
The __switch__, or network switch, is a device that allows multiple devices to connect to the network. It acts as a literal hub, connecting directly to the router or to another switch upstream. It helps avoid overloading the switch ports on your router or relocating devices to another room without running a cable from each one back to the router. Another common use case is to segment multiple networks that are managed by the same router. The __switch__, or network switch, is a device that allows multiple devices to connect to the network. It acts as a literal hub, connecting directly to the router or to another switch upstream. It helps avoid overloading the switch ports on your router or relocating devices to another room without running a cable from each one back to the router. Another common use case is to segment multiple networks that are managed by the same router.
There are generally two types of switches: There are generally two types of switches:
- **Unmanaged switches**, the most common. These are plug-and-play: you just plug them in and everything works automatically. - **Unmanaged switches**, the most common. These are plug-and-play: you just plug them in and everything works automatically.
- **Managed switches**. These offer a configuration interface (via command line or web UI), allowing you to fine-tune routing rules under the control of the router. They are powerful for creating virtual networks between your devices, but usually require more setup time and are less convenient than simple unmanaged switches. - **Managed switches**. These offer a configuration interface (via command line or web UI), allowing you to fine-tune routing rules under the control of the router. They are powerful for creating virtual networks between your devices, but usually require more setup time and are less convenient than simple unmanaged switches.
::alert{type="warning"} ::warning
:::list{type="warning"}
- __Warning:__ Make sure to use a switch with ports that match the speeds supported by your network devices. __Warning:__ Make sure to use a switch with ports that match the speeds supported by your network devices.
:::
:: ::
## Cables ## Cables
---
Cables are essential components of your network. Depending on their type and category, they can limit the bandwidth between devices, so they must be chosen to match your network's specifications. They also need to be compatible with your devices' ports. Cables are essential components of your network. Depending on their type and category, they can limit the bandwidth between devices, so they must be chosen to match your network's specifications. They also need to be compatible with your devices' ports.
@@ -91,11 +82,11 @@ On the other hand, if your device is limited to 100 Mb/s, a simple `CAT 5` cable
Nowadays, in new buildings, it is standard practice to install `CAT 6A` cables inside walls. This way, wall ports are ready to support 10 Gb/s over 100 meters. Nowadays, in new buildings, it is standard practice to install `CAT 6A` cables inside walls. This way, wall ports are ready to support 10 Gb/s over 100 meters.
---
### Optical Cables ### Optical Cables
Very thin but fragile, optical cables are increasingly appearing in home networks. It often starts with the fiber cable connecting your ISPs outlet to your box/router. They have several advantages: Very thin but fragile, optical cables are increasingly appearing in home networks. It often starts with the fiber cable connecting your ISPs outlet to your box/router. They have several advantages:
- Extremely compact - Extremely compact
- Zero electrical consumption (unlike copper, which loses energy as heat) - Zero electrical consumption (unlike copper, which loses energy as heat)
- No electromagnetic radiation (no shielding needed, no signal interference) - No electromagnetic radiation (no shielding needed, no signal interference)
@@ -105,29 +96,26 @@ For local networking, it's important to understand that several types of fiber c
For local networks, the recommended standard is a **multimode OM3 fiber with LC connectors**, paired with a **10G LC SFP+ transceiver**. This setup allows 10 Gb/s connections and is compatible with most devices featuring SFP+ ports. For local networks, the recommended standard is a **multimode OM3 fiber with LC connectors**, paired with a **10G LC SFP+ transceiver**. This setup allows 10 Gb/s connections and is compatible with most devices featuring SFP+ ports.
::alert{type="warning"} ::warning
:::list{type="warning"}
- __Warning:__ Make sure to use transceivers that are compatible with your devices (routers, switches, or other hardware). There is no universal standard yet, and manufacturers usually specify which brands are supported. __Warning:__ Make sure to use transceivers that are compatible with your devices (routers, switches, or other hardware). There is no universal standard yet, and manufacturers usually specify which brands are supported.
:::
:: ::
---
### DAC Cables ### DAC Cables
These are copper cables with integrated `transceivers`. They allow two SFP/SFP+ ports to communicate over short distances without using fragile fiber or RJ45 adapters. However, they consume more energy due to natural copper loss, which is non-negligible. These are copper cables with integrated `transceivers`. They allow two SFP/SFP+ ports to communicate over short distances without using fragile fiber or RJ45 adapters. However, they consume more energy due to natural copper loss, which is non-negligible.
---
### SFP+ Transceivers ### SFP+ Transceivers
These let you connect different types of cables to your SFP/SFP+ ports. Variants are available for: These let you connect different types of cables to your SFP/SFP+ ports. Variants are available for:
- Fiber optic - Fiber optic
- DAC - DAC
- RJ45 - RJ45
::alert{type="warning"} ::warning
:::list{type="warning"}
- RJ45 transceivers consume a lot of energy due to copper signal loss and can generate significant heat. Low-power models (under 2W) exist and are generally rated for longer cables (e.g., 80m instead of 30m). Surprisingly, these are preferred over short-distance models because they generate less heat and consume less energymaking them more compatible with sensitive devices. Using the wrong type can cause network degradation or even outages. __Warning:__ RJ45 transceivers consume a lot of energy due to copper signal loss and can generate significant heat. Low-power models (under 2W) exist and are generally rated for longer cables (e.g., 80m instead of 30m). Surprisingly, these are preferred over short-distance models because they generate less heat and consume less energy, making them more compatible with sensitive devices. Using the wrong type can cause network degradation or even outages.
:::
:: ::
@@ -1,13 +1,14 @@
--- ---
navigation: true
title: The ProloNAS title: The ProloNAS
description: Build a capable home server on a budget using an Intel N100 mini PC — a practical guide to getting started with self-hosting for under $130. description: Build a capable home server on a budget using an Intel N100 mini PC. A practical guide to getting started with self-hosting for under $130.
fluid: false
--- ---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# The ProloNAS :ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
::note
This article was written before the __massive__ increase in computer hardware prices we've been experiencing since late 2025. However, setting costs aside, it remains just as relevant.
::
When you decide to dive into the adventure of running your own home server, the same questions usually come up: _“Where should I start?”_, _“Isnt it expensive?”_. And often, people either give up or end up buying a proprietary NAS that theyll throw away a year later once they realize it only brings headaches and wasted money. When you decide to dive into the adventure of running your own home server, the same questions usually come up: _“Where should I start?”_, _“Isnt it expensive?”_. And often, people either give up or end up buying a proprietary NAS that theyll throw away a year later once they realize it only brings headaches and wasted money.
@@ -19,22 +20,19 @@ A mini PC for $100 + a USB dock for $50 that holds multiple hard drives = a comp
Thats all a **ProloNAS** is. Its then up to you to scale your storage capacity according to your needs. Thats all a **ProloNAS** is. Its then up to you to scale your storage capacity according to your needs.
![](/img/global/prolonas.svg) ![](/img/global/prolonas.svg)
## Example Hardware ## Example Hardware
- Mini PC **Note: choose 16 GB / 512 GB**: [Aliexpress](https://fr.aliexpress.com/item/1005008477986765.html) - Mini PC (**Note: choose 16 GB / 512 GB**): [Aliexpress](https://fr.aliexpress.com/item/1005008477986765.html)
- DAS (Direct Attached Storage) **Note: select “EU plug”**: [Aliexpress](https://fr.aliexpress.com/item/1005007933987260.html) - DAS (Direct Attached Storage) (**Note: select “EU plug”**): [Aliexpress](https://fr.aliexpress.com/item/1005007933987260.html)
- More refined alternative with a fan: [Amazon](https://www.amazon.fr/Boîtier-Disque-Ventilateur-Supportant-Capacité/dp/B0DD3GSSCX) - More refined alternative with a fan: [Amazon](https://www.amazon.fr/Boîtier-Disque-Ventilateur-Supportant-Capacité/dp/B0DD3GSSCX)
> *These are not affiliate links buy wherever you prefer.* > *These are not affiliate links, buy wherever you prefer.*
## Why a NAS? ## Why a NAS?
A **NAS** (Network Attached Storage) is a machine centered around storage, designed to be shared over a network.The idea is to have a **reliable and secure** storage space that serves as the backbone for your personal services and apps such as a self-hosted cloud like [Nextcloud](/serveex/cloud/nextcloud), a photo sync tool like [Immich](/serveex/cloud/immich), or a media server like [Plex](/serveex/media/plex). You can also store camera footage, backups, or even development projects on it. A **NAS** (Network Attached Storage) is a machine centered around storage, designed to be shared over a network.The idea is to have a **reliable and secure** storage space that serves as the backbone for your personal services and apps such as a self-hosted cloud like [Nextcloud](/serveex/cloud/nextcloud), a photo sync tool like [Immich](/serveex/cloud/immich), or a media server like [Jellyfin](/serveex/media/jellyfin). You can also store camera footage, backups, or even development projects on it.
### But why not just use a mini PC with an external hard drive? ### But why not just use a mini PC with an external hard drive?
@@ -44,7 +42,6 @@ A real NAS is built around **storage reliability**. It uses redundancy strategie
In short, a NAS lets you **host everything yourself** that you currently entrust to third parties while maintaining control, reliability, and data safety. In short, a NAS lets you **host everything yourself** that you currently entrust to third parties while maintaining control, reliability, and data safety.
## The Problem with Consumer NAS Systems ## The Problem with Consumer NAS Systems
Many brands offer “ready-to-use” NAS platforms: Synology, QNAP, Ugreen, and others. They promise simplicity and sleek web interfaces, but the reality is quite different. Many brands offer “ready-to-use” NAS platforms: Synology, QNAP, Ugreen, and others. They promise simplicity and sleek web interfaces, but the reality is quite different.
@@ -80,4 +77,3 @@ In short, you have **no control** over a product that isnt open, nor truly yo
As mentioned earlier: by adding a **DAS (drive hub)** and setting up a redundant storage system with [RAID](/general/storage/raid) and [ZFS](/general/storage/zfs), you can transform your mini PC into a robust and scalable NAS. As mentioned earlier: by adding a **DAS (drive hub)** and setting up a redundant storage system with [RAID](/general/storage/raid) and [ZFS](/general/storage/zfs), you can transform your mini PC into a robust and scalable NAS.
Enjoy ! Enjoy !
@@ -0,0 +1,2 @@
title: Linux tips for dummies
icon: i-lucide-terminal
@@ -0,0 +1,255 @@
---
title: Command line basics
description: Understand how a Linux command is built, learn the essential terminal commands, what their names mean, and get a cheat sheet to keep at hand.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
A server has no desktop, no icons and no mouse. Everything happens in a terminal, and that black window with a blinking cursor is the single thing that puts people off self-hosting. It shouldn't: the terminal is just a conversation. You type one line, the machine does exactly that and answers. Nothing more magic than a search bar, except it does far more and never hides an option behind three menus.
The good news is that you don't need to know a hundred commands. Ten of them cover almost everything you'll do on a home server, and they all follow the same pattern. Learn the pattern first, and every command you meet later becomes readable, even the ones you've never seen.
## How a command is built
Every command line, without exception, is the same sentence: **what to run**, **how to run it**, **what to run it on**.
```text [Anatomy of a command]
sudo apt install -y nano
│ │ │ │ └─ argument: what the command works on
│ │ │ └──── option: changes how it behaves
│ │ └──────────── subcommand: what the program should do
│ └──────────────── the program you're running
└───────────────────── run it with administrator rights
```
Read out loud, that line says "as an administrator, ask the package manager to install the nano package, and don't ask me to confirm". Spaces are what separate the pieces, which is why a folder named `My Backups` has to be quoted (`cd "My Backups"`) or the shell reads it as two different things.
### Options, short and long
Options change how a command behaves. They come in two flavours, and most commands accept both:
- **Short**, a single dash and a single letter: `ls -a`. They can be stacked, so `ls -l -a -h` is usually written `ls -lah`.
- **Long**, two dashes and a whole word: `ls --all`. Longer to type, but you can still tell what it does six months later, which is why they're the better choice in a script.
Some options expect a value right after them: `ssh-keygen -t ed25519` (`-t` for type), `rsync --exclude @eaDir`. And case matters, always. In `ls`, `-r` reverses the sort order while `-R` walks into subfolders. Two different things, one letter apart.
### Arguments and paths
The argument is the target: a file, a folder, a package name, an address. Many commands accept several at once, separated by spaces, which is what makes the terminal fast: `rm file1.txt file2.txt file3.txt` deletes three files in one go.
When the target is a place on the disk, you write it as a path, and there are a few shortcuts worth knowing:
| Path | Means |
| --- | --- |
| `/` | the root of the whole system, everything lives under it |
| `~` | your own home folder, `/home/username` |
| `.` | the folder you're currently in |
| `..` | the folder just above |
| `/var/log` | an **absolute** path, same result from anywhere |
| `logs/today` | a **relative** path, understood from where you currently stand |
Which folder holds what is a subject of its own, covered in [folders and partitions](/general/linux/filesystem).
The prompt itself tells you where you are: in `username@serveex:~/docker$`, you're logged in as `username` on the machine named `serveex`, inside the `docker` folder of your home. That final `$` means a normal user. If it ever shows `#`, you're root and every typo counts double.
### Getting help
Two habits make you independent from tutorials. `command --help` prints a quick summary of every option, and `man command` opens the full manual (`man` for *manual*), which you leave by pressing :kbd{value="Q"}.
::tip{icon=""}
✨ __Tip:__ three keyboard habits that change everything: :kbd{value="Tab"} completes the file or folder name you started typing, so you almost never type a full path; the :kbd{value="Up"} arrow brings back your previous commands, which saves retyping a long line for one character; and :kbd{value="Ctrl"} + :kbd{value="C"} stops whatever is currently running.
::
### Chaining commands
Once the pattern clicks, commands can be plugged into each other:
- `&&` runs the next one only if the previous one succeeded: `sudo apt update && sudo apt full-upgrade`
- `|`, the pipe, feeds the output of one command into another: `ls -l | grep backup` lists the folder, then keeps only the lines containing "backup"
- `>` writes the output into a file instead of the screen, and `>>` adds to the end of that file: `df -h > disk-report.txt`
## The commands you'll actually use
Most command names are abbreviations of an English phrase. Once you know what they stand for, they stop looking like keyboard noise.
### `pwd`, print working directory
Tells you where you are. It changes nothing, it just answers the question.
```bash [Terminal]
pwd
```
```console [Output]
/home/username/docker
```
### `ls`, list
Lists what's in the current folder. On its own it prints bare names, so it's almost always used with options: `-l` for the long format with sizes, dates and permissions, `-a` to also show hidden files (the ones starting with a dot), `-h` for sizes in K/M/G instead of raw bytes.
```bash [Terminal]
ls -lah
```
```console [Output]
total 20K
drwxr-xr-x 4 username username 4.0K Sep 5 10:12 .
drwxr-xr-x 18 username username 4.0K Sep 4 21:03 ..
-rw-r--r-- 1 username username 512 Sep 5 10:12 .env
-rw-r--r-- 1 username username 1.2K Sep 5 09:58 compose.yaml
drwxr-xr-x 3 username username 4.0K Sep 2 18:44 immich
```
The first column is the permissions, `d` at the very start meaning it's a folder. Then the owner, the size, the date of the last change, and the name.
### `cd`, change directory
Moves you around. With a path it goes there, with `..` it goes up one level, and with nothing at all it takes you back home.
```console [Terminal]
username@serveex:~/docker$ cd /var/log
username@serveex:/var/log$ cd ..
username@serveex:/$ cd
username@serveex:~$
```
Notice the prompt following you around: it always shows where you currently stand, so you rarely need `pwd` in practice.
### `mkdir`, make directory
Creates a folder. Several at once if you list them, and `-p` creates the whole chain of parents in one shot, which is the version you'll actually use.
```bash [Terminal]
mkdir backups
mkdir -p docker/immich/config
```
```console [Output]
```
Nothing. That's not a bug, it's the rule: most commands say nothing when they succeed and only speak up when something goes wrong. Silence is good news, and `ls` confirms the folder is there.
### `cp` and `mv`, copy and move
`cp` copies, `mv` moves. Same shape both times: first the source, then the destination. Copying a folder needs `-r`, for *recursive*, since a folder means everything inside it too. `mv` doubles as the rename command, because renaming a file is just moving it to a new name.
```bash [Terminal]
cp compose.yaml compose.yaml.bak
cp -r config/ config-backup/
mv old-name.txt new-name.txt
ls
```
```console [Output]
compose.yaml compose.yaml.bak config config-backup new-name.txt
```
Three silent commands, and `ls` showing the result: the copy sits next to the original, the folder was duplicated, and `old-name.txt` is gone because moving it to another name is exactly what renaming means.
### `rm`, remove
Deletes. There is no recycle bin, no undo, no confirmation dialog. `-r` deletes a folder and its contents, `-f` forces without asking.
::warning{to="/nonsense/bash/rm-confirmation"}
`rm -rf` is the command that wipes homelabs. It doesn't check, doesn't warn, and doesn't stop. Read the path twice before pressing :kbd{value="Enter"}, especially when the line starts with `sudo` and contains a `/` or a `*`. You can also prevent this by wrapping `sudo` in a small Bash function that asks **"are you sure?"** before it lets an `rm` through, covered in **rm confirmation guard**.
::
### `cat` and `nano`, read and edit
`cat` (short for *concatenate*) dumps a whole file to the screen, perfect for a short config. For anything longer, `less` scrolls through it (named as a joke on `more`, the older pager it replaced), and you quit it with :kbd{value="Q"}.
To actually change a file, `nano` opens a simple editor: arrows to move, :kbd{value="Ctrl"} + :kbd{value="O"} to save, :kbd{value="Ctrl"} + :kbd{value="X"} to leave.
```bash [Terminal]
cat .env
```
```properties [Output]
PUID=1000
PGID=1000
TZ=Europe/Paris
```
### `grep`, search inside files
`grep` stands for *global regular expression print*, which is a mouthful for "find me this text". You give it what to look for and where, and it prints every matching line. `-r` searches a whole folder, `-i` ignores upper and lower case, `-n` shows line numbers.
```bash [Terminal]
grep -rin "password" /home/username/docker
```
```console [Output]
/home/username/docker/immich/.env:6:DB_PASSWORD=changeme
/home/username/docker/vaultwarden/compose.yaml:14: ADMIN_PASSWORD=hunter2
```
Each line is the file, then the line number inside it, then the matching line itself. Very handy for the day you can't remember which stack holds a setting.
### `sudo`, run as administrator
*Substitute user do*. A normal user can't touch the system's files, which is exactly what protects you from wrecking the machine by accident. Prefixing a command with `sudo` runs that single command with administrator rights, and asks for your password the first time.
```bash [Terminal]
nano /etc/ssh/sshd_config
```
```console [Output]
Error writing /etc/ssh/sshd_config: Permission denied
```
```bash [Terminal]
sudo nano /etc/ssh/sshd_config
```
```console [Output]
[sudo] password for username:
```
::note
If a command answers `Permission denied`, that's usually the whole problem: it needed `sudo`. Resist the reflex of putting `sudo` on everything though, a file created as root will keep annoying you afterwards because your normal user no longer owns it.
::
## Cheat sheet
The ones worth keeping at hand, and where their names come from.
| Command | Short for | What it does |
| --- | --- | --- |
| `pwd` | print working directory | Shows where you are |
| `ls` | list | Lists files and folders |
| `cd` | change directory | Moves you somewhere else |
| `mkdir` | make directory | Creates a folder |
| `touch` | plain English | Creates an empty file, or refreshes its date |
| `cp` | copy | Copies a file or folder |
| `mv` | move | Moves or renames |
| `rm` | remove | Deletes, permanently |
| `cat` | concatenate | Prints a file to the screen |
| `less` | a pun on `more` | Scrolls through a long file |
| `nano` | the editor replacing Pico | Edits a file |
| `grep` | global regular expression print | Searches for text |
| `find` | plain English | Searches for files by name, size or date |
| `man` | manual | Opens a command's full documentation |
| `df` | disk free | Shows free space per partition |
| `lsblk` | list block devices | Draws the tree of disks and partitions |
| `du` | disk usage | Shows what a folder weighs |
| `ps` | process status | Lists running processes |
| `htop` | Hisham's `top` | Live view of CPU, RAM and processes |
| `kill` | plain English | Stops a process by its number |
| `chmod` | change mode | Changes a file's permissions |
| `chown` | change owner | Changes who owns a file |
| `sudo` | substitute user do | Runs one command as administrator |
| `apt` | advanced package tool | Installs, updates and removes packages |
| `systemctl` | control systemd | Starts, stops and enables services |
| `ssh` | secure shell | Opens a session on a remote machine |
| `scp` | secure copy | Copies files over SSH |
| `tar` | tape archive | Packs and unpacks archives |
| `wget` | web get | Downloads a file from a URL |
| `curl` | client URL | Sends a request to a URL |
| `history` | plain English | Lists the commands you typed before |
::tip{icon=""}
✨ __Tip:__ nobody memorises this. You'll look up the same three options for weeks, then one day realise you're typing them without thinking. Until then, `--help` and this table are perfectly legitimate.
::
@@ -0,0 +1,59 @@
---
title: Folders and partitions
description: How the Debian filesystem is organised, what each top-level folder holds, how partitions differ from folders, and the habits that keep a server tidy.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
Windows gives every disk its own letter. Linux doesn't: there is exactly one tree, it starts at `/`, and everything else hangs off it, including your other disks. A second drive isn't `D:`, it's *mounted* at a folder of the tree, `/mnt/data` for example, and from that point on it looks like any other folder. Odd at first, very practical afterwards, since a program never has to care which physical disk it's writing to.
## The tree
That tree isn't arbitrary either. Every Debian install has the same folders in the same places, which is why a tutorial written for someone else's server applies to yours.
| Folder | What's in it |
| --- | --- |
| `/home` | Users' files. Yours is `/home/username`, also written `~` |
| `/root` | The root account's own home, not to be confused with `/` |
| `/etc` | System configuration, all of it plain text files |
| `/var` | Data that grows: logs in `/var/log`, Docker in `/var/lib/docker` |
| `/tmp` | Temporary files, emptied at every reboot |
| `/usr` | The installed programs themselves, managed by `apt` |
| `/opt` | Software installed outside the package manager |
| `/mnt` and `/media` | Where extra disks get mounted, `/media` for removable ones |
| `/boot` | The kernel and the bootloader, on a small partition of its own |
| `/dev` | Your hardware, exposed as files (`/dev/sda` is a disk) |
| `/proc` and `/sys` | The kernel's live state, invented on the fly, not real files |
## Folders are not partitions
Partitions are a different question from folders. A minimal Debian install typically creates two, one for `/` and one for swap, so every folder above except `/boot` lives on the same partition and shares the same free space. Two commands to see the reality of it: `lsblk` draws the tree of disks and partitions, `df -h` shows how full each one is.
```bash [Terminal]
lsblk
```
```console [Output]
NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
sda 8:0 0 465.8G 0 disk
├─sda1 8:1 0 512M 0 part /boot/efi
├─sda2 8:2 0 461.3G 0 part /
└─sda3 8:3 0 4G 0 part [SWAP]
sdb 8:16 0 3.6T 0 disk
└─sdb1 8:17 0 3.6T 0 part /mnt/data
```
## A few habits worth taking
- **Give your Docker stacks one home, and keep them there.** `/srv` is the folder the standard reserves for data served by the machine, which makes it the tidiest choice for compose files and their bind mounts. [Serveex](/serveex/introduction) puts everything in `/srv/docker`, one folder per stack. What matters is picking one place and staying there, rather than scattering half of them into your home folder.
- **Your own files go in your home.** Scripts in `~/bin`, notes, downloads, anything personal. `/root` is the root account's home, not a convenient place to drop things.
- **Never edit anything under `/usr` or `/bin` by hand.** `apt` owns those, and your changes disappear at the next upgrade. What you're allowed to configure lives in `/etc`.
- **In `/etc`, prefer a drop-in file over editing the main one.** Many services read every `.conf` in a `something.d/` folder next to their main config, `/etc/ssh/sshd_config.d/` for instance. Your file then survives a package upgrade that rewrites the original.
- **Mount data disks by UUID, not by `/dev/sdb`.** Device letters are assigned in the order the kernel finds the disks, so they can swap after a reboot or a new drive. `lsblk -f` gives you the UUID to put in `/etc/fstab`.
- **Keep an eye on `/var`.** Docker images, container logs and system logs all pile up there, on the same partition as the rest. `du -sh /var/lib/docker` tells you what the containers weigh, `df -h` whether you should worry.
- **Don't create your files with `sudo` when you don't have to.** A file created as root inside your home stays owned by root, and you'll be fighting permission errors over it for weeks.
::note{to="/general/linux/cli-basics"}
Everything here assumes you can already move around a terminal. If `cd`, `ls` and `sudo` don't mean much yet, start with the **command line basics**.
::
@@ -0,0 +1,261 @@
---
title: Handy CLI tools
description: A handful of terminal tools worth installing on a home server, what each one replaces, and step-by-step instructions to install and use them.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
A minimal Debian install ships with the strict minimum, which means the tools you get are the ones from 1995. They work, but reading `df` output or hunting for what filled a disk with `du` is needlessly painful when better versions exist and cost nothing to install.
Everything below except the last one comes straight from Debian's repositories, so there's no third-party source to trust and `apt` keeps them updated along with the rest of the system.
::note{to="/general/linux/cli-basics"}
Every command here is typed in a terminal over SSH. If `sudo`, `apt` and `cd` don't mean much yet, start with the **command line basics**.
::
## The short version
| Tool | Replaces | What for |
| --- | --- | --- |
| `btop` | `top`, `htop` | Watching CPU, RAM and processes |
| `duf` | `df -h` | Free space, readable |
| `ncdu` | `du -sh` | Finding what filled the disk |
| `tldr` | `man` | The five commands you actually need |
| `lazydocker` | `docker ps` and friends | Managing containers over SSH |
| `ufw` | raw `iptables` | A firewall you can actually read |
## The impatient version
One line installs all the packaged ones, and each section below explains what you just got.
```bash [Terminal]
sudo apt update
sudo apt install btop duf ncdu tealdeer ufw
```
## `btop`, watching what the machine is doing
The modern replacement for `top` and `htop`: CPU, RAM, disks, network and processes on one screen, with graphs, colors and a working mouse. This is what you open when something feels slow.
::steps{level="4"}
#### Install it
```bash [Terminal]
sudo apt install btop
```
#### Run it
```bash [Terminal]
sudo btop
```
![btop showing CPU, memory, disks, network and processes](/img/global/linux/btop.png)
Click a process to select it, :kbd{value="Esc"} opens the menu, :kbd{value="Q"} quits. The `+` and `-` keys fold and unfold the panels if the screen feels crowded.
#### Done !
::
## `duf`, disk space that reads like a table
`df -h` prints every loop device Docker ever created and leaves you squinting at the columns. `duf` shows the same information grouped, aligned and colored, with a usage bar per filesystem.
::steps{level="4"}
#### Install it
```bash [Terminal]
sudo apt install duf
```
#### Run it
```bash [Terminal]
sudo duf
```
![duf listing local, network and special filesystems](/img/global/linux/duf.png)
Local disks, network shares and system mounts are grouped separately. Add `--only local` to hide the pseudo-filesystems Docker leaves behind.
#### Done !
::
## `ncdu`, finding what ate the disk
When `duf` tells you the disk is full, `ncdu` tells you why. It walks a folder, sorts everything by real size, and lets you drill down with the arrow keys instead of running `du -sh *` twenty times.
::steps{level="4"}
#### Install it
```bash [Terminal]
sudo apt install ncdu
```
#### Point it at a folder
```bash [Terminal]
sudo ncdu /srv/docker
```
Arrows to move, :kbd{value="Enter"} to open a folder, :kbd{value="D"} to delete the selected item, :kbd{value="Q"} to quit. On a big disk the first scan takes a moment, it's reading everything.
::warning
:kbd{value="D"} deletes immediately, with a single confirmation and no recycle bin. Run `ncdu` without `sudo` when you're only looking, so a mistyped key can't touch anything the system owns.
::
#### Done !
::
## `tldr`, the manual without the 400 lines
`man tar` is exhaustive and unreadable. `tldr tar` gives you the five commands people actually type, with a one-line explanation each. It's community-maintained examples rather than a substitute for the real manual, and on Debian the client is packaged as `tealdeer`.
::steps{level="4"}
#### Install it
```bash [Terminal]
sudo apt install tealdeer
```
#### Download the page cache
```bash [Terminal]
tldr --update
```
The examples are fetched once and stored locally, so the command works offline afterwards. Run it again every few months.
#### Ask it something
```bash [Terminal]
tldr rsync
```
#### Done !
::
## `lazydocker`, managing containers from the terminal
The one exception: it isn't packaged by Debian. It's a full text interface for Docker, containers, images, volumes and logs in one screen, with keys to restart, stop or follow the logs of anything. Handy when you're already in SSH and don't feel like opening Dockge.
::steps{level="4"}
#### Download the latest release
```bash [Terminal]
curl -Lo /tmp/lazydocker.tar.gz "https://github.com/jesseduffield/lazydocker/releases/latest/download/lazydocker_0.25.2_Linux_x86_64.tar.gz"
```
Check the [releases page](https://github.com/jesseduffield/lazydocker/releases) for the current version number, and take `arm64` instead of `x86_64` if the server is a Raspberry Pi or similar.
#### Install the binary
```bash [Terminal]
sudo tar -xzf /tmp/lazydocker.tar.gz -C /usr/local/bin lazydocker
rm /tmp/lazydocker.tar.gz
```
`/usr/local/bin` is the folder meant for software you install yourself, which is why `apt` never touches it.
#### Check it landed
```bash [Terminal]
lazydocker --version
```
#### Run it
```bash [Terminal]
sudo lazydocker
```
![lazydocker showing services, containers, images, volumes and a container's config](/img/global/linux/lazydocker.png)
It needs access to the Docker socket, hence the `sudo` unless your user is in the `docker` group. The keys worth knowing:
| Key | What it does |
| --- | --- |
| `1` to `6` | Jump to a panel: projects, services, containers, images, volumes, networks |
| Arrows | Move inside the panel, the right side follows the selection |
| :kbd{value="Enter"} | Focus the main panel on the right, :kbd{value="Esc"} comes back |
| `x` | Open the menu of everything you can do with what's selected |
| `m` | Follow the logs |
| `s` / `r` / `p` | Stop, restart, pause the selected container |
| `E` | Open a shell inside the container |
| `d` | Remove it |
| `b` | Bulk commands, pruning images and volumes among others |
| `/` | Filter the list |
| `+` and `_` | Grow or shrink the panels |
| `q` | Quit |
Case matters: `E` opens a shell in the container, `e` hides the stopped ones.
The [full list](https://github.com/jesseduffield/lazydocker/blob/master/docs/keybindings/Keybindings_en.md) is in the project's documentation.
::note
Being outside `apt` also means it won't be updated by `apt full-upgrade`. Repeat these steps when you want a newer version.
::
#### Done !
::
## `ufw`, a firewall you can actually read
Debian's firewall (`iptables`/`nftables` under the hood) is powerful and unreadable directly. `ufw`, *uncomplicated firewall*, is a thin layer on top that turns it into short, plain-English rules, block everything by default and open only what you actually expose.
::steps{level="4"}
#### Install it
```bash [Terminal]
sudo apt install ufw
```
#### Set the default policy
```bash [Terminal]
sudo ufw default deny incoming
sudo ufw default allow outgoing
```
Nothing gets in unless a rule says so, everything the server itself initiates still goes out normally.
#### Allow what you actually need
```bash [Terminal]
sudo ufw allow OpenSSH
sudo ufw allow 443/tcp
```
`OpenSSH` is a built-in profile that matches the SSH port, no need to remember which one. Add one `allow` per port you expose, [SWAG](/serveex/core/swag) on `443` for instance.
::warning
Allow SSH **before** enabling the firewall, in the next step. Enable it first and the very connection you're typing in gets cut, with no screen left plugged in to fix it.
::
#### Enable it
```bash [Terminal]
sudo ufw enable
```
#### Check the rules
```bash [Terminal]
sudo ufw status verbose
```
```console [Output]
Status: active
Logging: on (low)
Default: deny (incoming), allow (outgoing), disabled (routed)
To Action From
-- ------ ----
22/tcp (OpenSSH) ALLOW IN Anywhere
443/tcp ALLOW IN Anywhere
```
#### Done !
::
+2
View File
@@ -0,0 +1,2 @@
title: Serveex
icon: i-noto-microscope
+321
View File
@@ -0,0 +1,321 @@
---
title: Introduction
description: Introduction to Serveex, a personal homelab project to self-host everyday services using Debian and Docker, replacing cloud services as Google, Apple or Netflix.
navigation:
icon: i-lucide-bookmark
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
## A Home Lab by a Beginner, for Beginners
![](/img/serveex/serveex-server.svg)
**Serveex** is primarily a personal project aimed at hosting as many everyday services as possible at home, without relying on proprietary platforms (Google, Apple, Netflix, etc.). The goal was to experiment, learn, and document every step along the way. This is purely a scientific project and is not intended for production use.
A big thanks to **Nipah** for sharing his infinite knowledge and, above all, for his patience.
::note{icon=""}
📋 **Prerequisites:**
- Have [an online VPS](https://www.it-connect.fr/les-serveurs-prives-virtuels-vps-pour-les-debutants/) or a local machine: ideally a mini PC (you can find N100 models for around €100), but it also works on a laptop or [a virtual machine](https://openclassrooms.com/fr/courses/2035806-virtualisez-votre-architecture-et-vos-environnements-de-travail/6313946-installez-virtualbox). The [Freebox Delta/Ultra offer virtual machines](https://next.ink/3493/machines-virtuelles-et-freebox-delta-comment-heberger-votre-premiere-page-web/).
- Know how to configure [NAT rules on a router and assign DHCP leases](/general/networking/nat)
- Know how to configure the [DNS zone of a domain name](/general/networking/dns)
::
<div align="center">
<img src="/img/serveex/serveex.svg" align="center" width="700">
</div>
The goal is to be easily deployable and easy to migrate, so here is its structure:
### The Core of the Server
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card{icon="i-logos-debian" title="Operating System" to="/serveex/core/installation"}
Install and configure Debian 13
::
::card{icon="i-logos-docker-icon" title="Using apps container" to="/serveex/core/docker"}
Install Docker
::
::card
---
icon: i-carbon-container-registry
title: Container manager
to: "/serveex/core/docker#installer-dockge-pour-gérer-et-déployer-les-conteneurs"
ui:
icon: text-[#74C2FF]
---
Install and deploy Dockge
::
::card
---
icon: i-simple-icons-wireguard
title: VPN
to: /serveex/core/wireguard
ui:
icon: text-[#88171A]
---
Install and deploy Wireguard
::
::card{icon="i-noto-globe-showing-americas" title="Reverse Proxy" to="/serveex/core/swag"}
Expose your services with SWAG
::
::
### Security
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card{icon="i-noto-locked-with-key" title="Forward Auth" to="/serveex/security/tinyauth"}
Install and deploy TinyAuth
::
::card{icon="i-noto-identification-card" title="Passwordless SSO" to="/serveex/security/pocket-id"}
Install and deploy Pocket ID
::
::card{icon="i-logos-cloudflare-icon" title="Zero Trust" to="/serveex/security/cloudflare"}
Install and deploy Cloudflared
::
::
### Monitoring
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card
---
icon: i-solar-pulse-linear
title: Service Status
to: /serveex/monitoring/uptime-kuma
ui:
icon: text-[#5CDD8B]
---
Install and deploy Uptime-Kuma
::
::card
---
icon: i-lucide-logs
title: Log Management
to: /serveex/monitoring/dozzle
ui:
icon: text-[#FFA600]
---
Install and deploy Dozzle
::
::card{icon="i-noto-rabbit" title="Connection Management" to="/serveex/monitoring/speedtest-tracker"}
Install and deploy Speedtest Tracker
::
::card
---
icon: i-lucide-chart-column-decreasing
title: Resource Status
to: /serveex/monitoring/beszel
ui:
icon: text-[#747bff]
---
Install and deploy Beszel
::
::card
---
icon: i-lucide-circle-power
title: Wake on Lan
to: /serveex/monitoring/upsnap
ui:
icon: text-[#5BDAFD]
---
Install and deploy UpSnap
::
::
### Media
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card
---
icon: i-simple-icons-jellyfin
title: Media
to: /serveex/media/jellyfin
ui:
icon: text-[#00A4DC]
---
Install and deploy Jellyfin
::
::card
---
icon: i-cbi-qbittorrent
title: Seedbox
to: /serveex/media/qbittorrent
ui:
icon: text-[#2F67BA]
---
Install and deploy Qbittorrent
::
::card
---
icon: i-cbi-radarr
title: Automation
to: /serveex/media/servarr
ui:
icon: text-[#FFCB3D]
---
Install and deploy the Servarr stack
::
::
### Cloud Drive & Photos
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card
---
icon: i-cib-nextcloud
title: Drive
to: /serveex/cloud/nextcloud
ui:
icon: text-[#0082C9]
---
Install and deploy Nextcloud
::
::card
---
icon: i-simple-icons-immich
title: Photos
to: /serveex/cloud/immich
ui:
icon: text-[#4250AF]
---
Install and deploy Immich
::
::
### Files & Sharing
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card{icon="i-noto-open-file-folder" title="File Explorer" to="/serveex/files/file-browser-quantum"}
Install and deploy File Browser Quantum
::
::card
---
icon: i-carbon-share
title: Sharing
to: /serveex/files/pingvin
ui:
icon: text-[#46509E]
---
Install and deploy Pingvin
::
::
### Development Tools
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card{icon="i-logos-visual-studio-code" title="Visual Studio Code" to="/serveex/development/code-server"}
Install and deploy code-server
::
::card
---
icon: i-simple-icons-forgejo
title: Git Repository
to: /serveex/development/forgejo
ui:
icon: text-[#FB923C]
---
Install and deploy Forgejo
::
::card{icon="i-noto-hammer-and-wrench" title="Tools" to="/serveex/development/it-tools"}
Install and deploy IT Tools
::
::
### Useful Applications
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card
---
icon: i-cbi-adguard
title: Ad-blocking DNS and Filters
to: /serveex/apps/adguard
ui:
icon: text-[#68BC71]
---
Install and deploy Adguard Home
::
::card
---
icon: i-cbi-bitwarden
title: Password Manager
to: /serveex/apps/vaultwarden
ui:
icon: text-[#175DDC]
---
Install and deploy Vaultwarden
::
::
### Advanced
:::div{class="relative"}
:ellipsis{left=0px width=40rem top=10rem blur=140px}
:::
::card-group
::card{icon="i-noto-key" title="SSO & MFA" to="/serveex/advanced/authentik"}
Install and deploy Authentik
::
::card{icon="i-noto-crystal-ball" title="Multi-host Docker manager" to="/serveex/advanced/arcane"}
Install and deploy Arcane
::
::
## Coming Soon
- Homepage, to have all your services at a glance and access them easily
- Zensical, how to write and organize your own documentation
@@ -0,0 +1,2 @@
title: Server core
icon: i-lucide-server-cog
@@ -0,0 +1,346 @@
---
title: Debian 13
description: Step-by-step guide to install Debian 13 on a home server and set up SSH access, essential packages, and a ready-to-use base system.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
[Debian 13 "Trixie"](https://www.debian.org/) is the base this whole guide sits on. It's a plain, boring, stable Linux, and for a homelab that's exactly the point: package versions stay frozen for the life of the release, security patches keep coming for about five years counting LTS, so the machine keeps running for years instead of needing a rebuild every few months.
The other reason is that it stays out of your way. Appliance systems like Unraid, TrueNAS or Synology's DSM put their own layer on top of Docker, and the day you need something their interface doesn't expose, you're stuck working around it. Debian is just a server: you install what you want, where you want, and nothing is hidden behind someone else's UI. It also happens to be what most self-hosted projects target first, so their docs hand you `apt` commands that work as-is, Docker publishes an official Debian repository, and any error message you paste into a search engine already has years of answers behind it. A minimal install is light enough to leave nearly all the RAM and CPU of a small N100 box to your containers.
![picture](/img/serveex/server.svg)
## Installation
::note{icon="" to="/general/linux/cli-basics"}
📋 __Prerequisite:__ everything past this point happens in a terminal, so you should be comfortable with the basics: moving around with `cd`, listing with `ls`, editing a file with `nano`, and reading what a command tells you when it fails. If any of that is new, **start with the command line basics** and come back.
::
### BIOS setup
Press :kbd{value="Del"} or :kbd{value="F2"} right after powering on to open the firmware setup (the boot screen usually says which key it is). Most machines also have a one-shot boot menu, often :kbd{value="F12"}, :kbd{value="F11"} or :kbd{value="F8"}, which lets you boot the USB installer once without touching the permanent boot order. Debian documents the general procedure in its [installation manual](https://www.debian.org/releases/forky/amd64/ch03s06.en.html), and here is what matters before you install:
- **Boot mode.** Prefer native UEFI. The important part is that the installer boots in the *same* mode you intend to run the server in, because UEFI uses GPT partitioning while legacy BIOS (and UEFI in CSM mode) uses a DOS partition table, and a mismatch installs the bootloader in the wrong place. Watch out on multi-boot machines: the default boot mode for removable devices is often not the one used for internal disks.
- **Secure Boot** can stay enabled. Debian ships a Microsoft-signed shim bootloader, so it boots fine as-is.
- **SATA mode** should be `AHCI`, not `RAID` / Intel RST, otherwise Linux may not see your drives at all. Changing this after installing another OS on the same disk will stop that OS from booting, so set it before you start.
- **Restore on AC power loss** so the server comes back by itself after an outage instead of waiting for someone to press the button. The setting lives in *Power Management*, *APM Configuration* or *Advanced* depending on the board, under a name like *Restore on AC Power Loss*, *AC Power Recovery*, *After Power Failure*, *AC Back Function* or *S0 state*. Set it to **Power On**, not *Last State*, which would leave the machine off if the outage caught it during a shutdown.
- **Wake on LAN**, if you want to power the machine up remotely instead of walking to it. Same *Power Management* menu: set *Wake on LAN*, *Power On By PCI-E/PCI* or *Resume by PCI-E Device* to **Enabled**, then disable *ErP* / *EuP Ready* and *Deep Sleep* / *Deep Sx*, which cut power to the network card once the machine is off and would keep it deaf to the magic packet. Debian also needs the card told to listen, see [Going further](#going-further).
- **Virtualization** (`VT-x` / `AMD-V`, plus `VT-d` for passthrough) costs nothing to turn on now and saves a trip back into the BIOS the day you want to run a VM. Docker itself doesn't need it on Linux.
::note
If you're dual-booting Windows, disable its *fast startup*: it leaves the filesystem in a state Linux can corrupt, and Windows Update likes to silently turn it back on.
::
### Download the ISO and write it to a USB stick
::steps{level="4"}
#### Download the netinst image
Grab the `amd64` **netinst** image from [debian.org](https://www.debian.org/download.en.html). It's around 700 MB and pulls the rest of the packages from the network during install, which is what you want on a server that's plugged into ethernet: you get current packages instead of installing from a months-old snapshot and patching afterwards. The full DVD images only make sense if the machine has no network during setup.
#### Write it with Rufus
On Windows, write it with [Rufus](https://rufus.ie/) (portable, no install needed). Plug in a USB stick of 2 GB or more, keeping in mind **it will be wiped entirely**, then:
- **Device**: your USB stick. Check the capacity twice, Rufus happily writes to the wrong drive if you let it.
- **Boot selection**: `SELECT`, then pick the Debian ISO you just downloaded.
- **Partition scheme**: this has to match the boot mode you set in the BIOS above. `GPT` for UEFI, `MBR` only if you're staying on legacy/CSM. The target system field follows automatically.
- Leave the format options at their defaults, then hit `START`. If Rufus asks how to write the image, keep the recommended *ISO Image mode*.
![Rufus configured to write the Debian ISO](/img/serveex/install/rufus.png)
_Screenshot from [this bootable USB guide on DEV Community](https://dev.to/devops2808/how-to-create-bootable-usb-installer-for-debian-12-4f66)._
Writing takes a few minutes.
#### Done !
::
### Install Debian
Boot the USB stick (one-shot boot menu from the BIOS section) and pick **Install**, the text installer. The goal here is a minimal headless server: no desktop, no graphical session, nothing but a shell reachable over SSH. The screen and keyboard you're using right now are only needed for this one install, after that the machine runs blind in a corner. The [official installation guide](https://www.debian.org/releases/trixie/amd64/ch06s03.en.html) documents every screen.
![Debian installer boot menu, Install selected](/img/serveex/install/debian-install-boot.png)
::steps{level="4"}
#### Language, country, keyboard
Nothing special. The keyboard layout is the one you're physically typing on, which is easy to get wrong if you picked English but type on AZERTY.
#### Network and hostname
A wired connection gets configured over DHCP by itself. When it asks for a **hostname**, give the machine a real name (`serveex`, `nas`...), you'll see it in every SSH prompt afterwards. The **domain** can be left empty, or set to something like `lan` if you already use one at home.
![Debian installer hostname screen](/img/serveex/install/debian-install-hostname.png)
#### Root password and user account
Leave the **root password empty**. Debian then disables the root account, installs `sudo` and puts your user in it, which is the safer default and saves you a round of setup later.
Then create your user: full name, username, password. This is the account you'll SSH into. Avoid `admin` as a username, it's reserved on Debian and the installer will reject it.
#### Clock
Confirm the timezone guessed from your country.
#### Partitioning
*Guided, use entire disk* on the system drive, then *All files in one partition*, which gives you one big `/` plus a swap partition. Separate `/home` or `/var` partitions buy you very little here and mostly guarantee that one fills up while the others sit half empty. Pick LVM only if you already know you want snapshots or to grow volumes later. Your data disks are not touched at this stage, you'll mount them afterwards.
Finish with *Finish partitioning and write changes to disk*, then confirm with *Yes*: this is the point of no return for that disk.
![Debian installer partitioning scheme, all files in one partition](/img/serveex/install/debian-install-partition.png)
::tip{icon="" to="/general/linux/filesystem"}
__Tip:__ what actually lives on that one partition, and why `/srv/docker` is where this guide puts every stack, is covered in **folders and partitions**.
::
#### Mirror and surveys
Answer *No* to *Scan another installation medium?*, everything else comes from the network. For the mirror, pick any one in your country, or `deb.debian.org` which routes to a nearby one automatically, and leave the HTTP proxy field empty unless you actually have one. The popularity contest (anonymous package statistics) is yes or no, no consequence either way.
#### Software selection (tasksel)
The screen that actually decides whether your server stays minimal. Uncheck **everything**, in particular `Debian desktop environment` and `GNOME`, which are ticked by default and would drag in gigabytes of packages plus a graphical session you will never display. Keep exactly two boxes: **`SSH server`**, your only way in from now on, and **`standard system utilities`**, which the rest of this guide assumes.
::warning
Boxes are ticked and unticked with :kbd{value="Space"}, never :kbd{value="Enter"}. :kbd{value="Enter"} validates the whole screen and moves on, so pressing it on the desktop entry installs GNOME instead of removing it, and you get a graphical server you then have to strip by hand. Use :kbd{value="Tab"} to reach `Continue` once the two boxes above are the only ones checked.
::
![Debian installer software selection with only SSH server and standard system utilities checked](/img/serveex/install/debian-install-tasksel.png)
#### GRUB
Install it on the disk you just partitioned (`/dev/sda`, `/dev/nvme0n1`...), not on a partition.
![Debian installer asking which device to install the GRUB boot loader to](/img/serveex/install/debian-install-grub.png)
#### Done !
::
::note
Before rebooting, take a minute to give the server a **fixed address** in your router. Everything that comes later points at it: your SSH shortcuts, the reverse proxy, the bookmarks to each service. On a plain DHCP lease that address changes on its own eventually and all of it breaks at once.
The clean way is a DHCP reservation, which ties the address to the server's MAC address while leaving the router in charge of the addressing. See [NAT & DHCP](/general/networking/nat) for where to find it in your router's interface.
::
_Installer screenshots from [howtoforge.com's Debian minimal server guide](https://www.howtoforge.com/tutorial/debian-minimal-server/)._
### Connect over SSH
The server has no screen from now on, everything goes through SSH. These steps get you in, then make sure nobody else can be.
::steps{level="4"}
#### Connect from another machine
Remove the USB stick and reboot. The address to use is the one you reserved in the router just before, `192.168.1.42` in the examples below.
Everything from here happens from another machine on your local network, not on the server. Windows and macOS both ship an SSH client, so there's nothing to install: open **PowerShell** on Windows, or **Terminal** on macOS, and type the same command.
```bash [Terminal]
ssh [email protected]
```
The first connection asks you to confirm the server's fingerprint, which is normal, answer `yes`. It gets stored in `~/.ssh/known_hosts` and you won't be asked again.
::note
If the connection is refused, the `SSH server` box was probably left unchecked at the tasksel screen. Plug a screen back in, log in locally and run `sudo apt install openssh-server`.
::
The screen and keyboard are no longer needed. Unplug them, the machine can go live in its corner.
#### Log in with a key instead of a password
Passwords over SSH get brute-forced the moment the port is reachable from outside, and typing one on every connection gets old fast. Still on the other machine, generate a key if you don't already have one:
```bash [Terminal]
ssh-keygen -t ed25519
```
Press :kbd{value="Enter"} to accept the default path, and set a passphrase (it protects the key file itself, your system will remember it after the first unlock). Then copy the public half to the server. Windows has no `ssh-copy-id`, so it pushes the key over the connection instead:
::code-group
```bash [macOS]
ssh-copy-id [email protected]
```
```bash [Windows]
type $env:USERPROFILE\.ssh\id_ed25519.pub | ssh [email protected] "mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"
```
```bash [Linux]
ssh-copy-id [email protected]
```
::
It asks for your password one final time. Reconnect to check that it no longer does:
```bash [Terminal]
ssh [email protected]
```
#### Close the door behind you
Once the key works, turn off password logins and direct root access. On the server:
```bash [Terminal]
sudo nano /etc/ssh/sshd_config.d/hardening.conf
```
```properties [hardening.conf]
PasswordAuthentication no
PermitRootLogin no
KbdInteractiveAuthentication no
```
A file in `sshd_config.d/` is read on top of the main config, so your changes survive a package upgrade rewriting `/etc/ssh/sshd_config`. Apply it:
```bash [Terminal]
sudo systemctl restart ssh
```
::warning
Keep your current SSH session open while you test. Open a **second** terminal and connect again: if the key stopped working, the still-open session is your way back in to fix the config. Close it before you've checked and a typo locks you out of your own server, leaving the screen and keyboard as the only way back.
::
::note
The door is now closed for every other machine too, including the next one you'll want to connect from. To let a new one in, set `PasswordAuthentication yes` back in `hardening.conf`, restart SSH, run the two key steps above from that machine, then set it to `no` again and restart SSH one last time.
::
#### Done !
::
### Wake the server up remotely
A machine that runs 24/7 for two hours of actual use burns power, spins fans and wears drives for nothing. Wake on LAN lets you shut it down properly when you're done and bring it back in a few seconds without walking to it: the network card stays powered in standby, listening for one specific broadcast (the *magic packet*) carrying the server's MAC address, and switches the machine on when it sees it. Handy for a backup target you only need at night, or a media server nobody watches during the day.
Two conditions before you start: the machine has to be wired to ethernet, WiFi cards almost never support this, and the packet has to be sent from the same local network, since a broadcast doesn't cross a router. The BIOS side was covered in [BIOS setup](#bios-setup), here is the Debian side.
::steps{level="4"}
#### Find the interface and its MAC address
```bash [Terminal]
ip -br link
```
You get something like `enp1s0 UP aa:bb:cc:dd:ee:ff`. Keep both: the interface name for the commands below, the MAC address for the machine that will send the packet.
#### Check the card supports it
```bash [Terminal]
sudo apt install ethtool
sudo ethtool enp1s0 | grep -i wake
```
The answer looks like `Supports Wake-on: pumbg` then `Wake-on: d`. The letter that matters is **g**, for magic packet. If the *Supports* line doesn't have it, the card can't do it and there's nothing to configure. `Wake-on: d` simply means disabled, which the next step fixes.
#### Turn it on
```bash [Terminal]
sudo ethtool -s enp1s0 wol g
```
Run the check again, `Wake-on` should now be `g`. This setting is reset at every boot, so it needs to be reapplied automatically.
#### Make it survive reboots
```bash [Terminal]
sudo nano /etc/systemd/system/wol.service
```
```ini [wol.service]
[Unit]
Description=Enable Wake on LAN
[Service]
Type=oneshot
ExecStart=/usr/sbin/ethtool -s enp1s0 wol g
[Install]
WantedBy=basic.target
```
```bash [Terminal]
sudo systemctl daemon-reload
sudo systemctl enable --now wol.service
```
#### Wake it up
Shut the server down with `sudo poweroff`, then send the magic packet from another machine on the network. On macOS and Linux, the `wakeonlan` package does it in one command:
```bash [Terminal]
wakeonlan aa:bb:cc:dd:ee:ff
```
Windows has no built-in sender, so the simplest route there is a phone app: any of the free *Wake on LAN* apps takes the MAC address and works the same way. The server should start within a couple of seconds.
#### Done !
::
::note
Waking it from outside your home is another story, and going through a VPN doesn't help if that VPN runs on the server itself: the tunnel is down for as long as the machine is off. The way around it is to forward a UDP port on the router (7 or 9, the usual Wake on LAN ports) to the server, then send the packet to your public address from an app that handles it, [WolOn](https://wolon.app/) for instance. The [NAT rule](/general/networking/nat) is a plain one, `UDP 9` from the outside to `192.168.1.42:9` on the inside. The router still has to point that IP at the right MAC address while the machine is off, which is why some of them expose a static ARP entry, or a Wake on LAN button of their own that saves you the port forward entirely. Worth checking your router first.
::
### Keep it up to date
Debian doesn't update itself. Every month or so, or whenever you think about it, four commands over SSH:
::steps{level="4"}
#### Refresh the package lists
```bash [Terminal]
sudo apt update
```
Nothing is installed at this point, `apt` only asks the mirrors what's available and tells you how many packages are behind.
#### Apply the updates
```bash [Terminal]
sudo apt full-upgrade
```
`full-upgrade` is preferred over plain `upgrade` because it accepts removing a package when that's what it takes to move another one forward, which does happen on a server that lives for years. Read the summary before answering yes, it lists exactly what gets removed.
#### Clean up behind them
```bash [Terminal]
sudo apt autoremove --purge
```
Every kernel update leaves the previous one installed, and `/boot` is a small partition that eventually fills up and breaks the next upgrade. Do this every single time, not once in a while. `--purge` also drops the config files of the packages being removed.
#### Reboot if the kernel moved
```bash [Terminal]
sudo reboot
```
A kernel or libc update only takes effect after a restart. Everything else applies immediately, so this is only needed when the upgrade touched one of those, and it's worth planning for a moment when nothing depends on the machine.
#### Done !
::
::tip
If you don't need to watch what's going on, the first three steps fit on one line, `&&` stopping the chain as soon as one of them fails:
```bash [Terminal]
sudo apt update && sudo apt full-upgrade -y && sudo apt autoremove --purge -y
```
`-y` answers yes to every question, including the day an upgrade proposes to remove something you would rather have kept, so keep it for routine rounds. Append `&& sudo reboot` to get the restart out of the way too.
::
For security patches without having to think about it, `sudo apt install unattended-upgrades` then `sudo dpkg-reconfigure -plow unattended-upgrades` applies them on its own every night. Note that all of this only covers the system: your containers are updated separately, from Dockge.
### Going further
- [Everything About Remote Console Access (SSH)](https://www.digitalocean.com/community/tutorials/ssh-essentials-working-with-ssh-servers-clients-and-keys)
- Optional - [UPS Client in Case of Power Outage](https://www.sindastra.de/p/2078/how-to-connect-linux-server-to-synology-ups-server) / [also here](https://www.reddit.com/r/synology/comments/gtkjam/use_synology_nas_as_ups_server_to_safely_power/)
::tip{icon="" to="/general/linux/handy-tools"}
✨ __Tip:__ a handful of terminal tools worth adding on top of a minimal install, `btop`, `duf`, `ufw` and a few more, are covered in **handy CLI tools**.
::
@@ -1,29 +1,24 @@
--- ---
navigation: true
title: Docker title: Docker
description: Install Docker and Dockge on Debian to deploy and manage self-hosted services with simple container stacks. description: Install Docker and Dockge on Debian to deploy and manage self-hosted services with simple container stacks.
main:
fluid: false
--- ---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Docker
Docker, to install deployable services in seconds and manage them with just a few commands or clicks.
::alert{type="info"} :ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
🎯 __Goals:__
- Install [Docker](https://www.docker.com/) Every app in this guide, [Jellyfin](/serveex/media/jellyfin), [Vaultwarden](/serveex/apps/vaultwarden), [Immich](/serveex/cloud/immich), to name a few, comes with its own list of dependencies, its own version of Python or Node, its own quirks. Installing all of that directly on Debian works for a while, until two apps want a different version of the same library, or removing one leaves files scattered across the system with no clean way back.
- Install [Dockge](https://github.com/louislam/dockge) to manage stacks
- Install [Watchtower](https://github.com/containrrr/watchtower) to update containers A **container** sidesteps the problem: it packages an app together with everything it needs to run, isolated from the rest of the system and from every other container. Starting one doesn't touch Debian's own packages, and removing it is a single command that leaves nothing behind. It's not a virtual machine either, there's no second operating system to boot or resources to pre-allocate: a container shares the host's kernel and starts in about a second, using only the RAM and CPU the app inside it actually needs.
::
**Docker** is the tool that builds, starts and manages these containers. Point it at an *image*, a ready-made snapshot of an app maintained by its developers, and it downloads it and runs it in one command. The rest of Serveex is built entirely on it: every app from here on is one Docker container, or a handful of them working together.
![picture](/img/serveex/docker.svg) ![picture](/img/serveex/docker.svg)
## Install Docker ## Install Docker
--- ::steps{level="3"}
Add the Docker repositories and GPG key: ### Add the Docker repository and GPG key
```sh ```bash [Terminal]
# Add Docker's official GPG key: # Add Docker's official GPG key:
sudo apt-get update sudo apt-get update
sudo apt-get install ca-certificates curl sudo apt-get install ca-certificates curl
@@ -36,57 +31,61 @@ echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docke
sudo apt-get update sudo apt-get update
``` ```
Install the packages: ### Install the packages
```sh ```bash [Terminal]
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
``` ```
That's it! ### Done !
::
**More options:** [Install Docker for Debian 13](https://docs.docker.com/engine/install/debian/) **More options:** [Install Docker for Debian 13](https://docs.docker.com/engine/install/debian/)
::alert{type="info" icon="exclamation-circle"} ::note
:::list{type="info"}
- From here on, we assume the stacks are installed in the `/docker` folder, created using the command: From here on, we assume the stacks are installed in the `/srv/docker` folder, created using the command:
::: ```bash [Terminal]
```sh sudo mkdir /srv/docker
sudo mkdir /docker ```
:: ::
## Install [Dockge](https://github.com/louislam/dockge) to manage and deploy containers ## Install [Dockge](https://github.com/louislam/dockge) to manage and deploy containers
---
[Dockge](https://github.com/louislam/dockge) is a web tool to create, configure, launch, and manage Docker containers. It's a simple, intuitive interface thats lighter and easier for beginners than using the CLI or Portainer. [Dockge](https://github.com/louislam/dockge) is a web tool to create, configure, launch, and manage Docker containers. It's a simple, intuitive interface thats lighter and easier for beginners than using the CLI or Portainer.
![picture](/img/serveex/dockge.png) ![picture](/img/serveex/dockge.png)
### Configuration ### Configuration
File structure we will create: ::file-tree
---
label: File structure we will create
tree:
/:
- srv:
- docker:
- dockge:
- compose.yml
---
::
```sh ::steps{level="4"}
root #### Create the stack folder
└── docker
└── dockge
└── compose.yml
```
Create the stack folder: ```bash [Terminal]
cd /srv/docker
```sh
cd /docker
sudo mkdir dockge sudo mkdir dockge
``` ```
Then create the `compose.yml` file in this folder using `vim`: #### Create the compose file
```sh ```bash [Terminal]
cd /docker/dockge cd /srv/docker/dockge
sudo vi compose.yml sudo nano compose.yml
``` ```
Press `i` to enter insert mode and paste the following: Paste the following:
```yaml ```yaml [compose.yaml]
--- ---
services: services:
dockge: dockge:
@@ -98,18 +97,18 @@ services:
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- /docker/dockge/data:/app/data - /srv/docker/dockge/data:/app/data
- /docker:/docker - /srv/docker:/srv/docker
environment: environment:
- DOCKGE_STACKS_DIR=/docker - DOCKGE_STACKS_DIR=/srv/docker
``` ```
Press `Esc` and type `:x` to save and exit. Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
To launch the container: #### Launch the container
```sh ```bash [Terminal]
cd /docker/dockge cd /srv/docker/dockge
sudo docker compose up -d sudo docker compose up -d
``` ```
@@ -117,20 +116,25 @@ Then go to `http://yourserverip:3555` in your browser to access the login page.
More info on [Dockge and how to use it](https://github.com/louislam/dockge) More info on [Dockge and how to use it](https://github.com/louislam/dockge)
And there you go — Docker and a tool to easily manage your containers are ready! #### Done !
::
And there you go! Docker and a tool to easily manage your containers are ready!
## [Watchtower](https://watchtower.nickfedor.com/), to auto-update containers ## [Watchtower](https://watchtower.nickfedor.com/), to auto-update containers
---
Watchtower is a container that checks for updates and pulls new images automatically, just by adding a label in your containers `compose.yml` files. Watchtower is a container that checks for updates and pulls new images automatically, just by adding a label in your containers `compose.yml` files.
### Configuration ### Configuration
::steps{level="4"}
#### Create the stack
- Open Dockge in your browser - Open Dockge in your browser
- Click `compose` - Click `compose`
- Name the stack `watchtower` - Name the stack `watchtower`
- Paste the config below into the default config area in Dockge - Paste the config below into the default config area in Dockge
```yaml ```yaml [compose.yaml]
--- ---
services: services:
watchtower: watchtower:
@@ -153,9 +157,11 @@ services:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
``` ```
Then fill in the `.env` section in Dockge with the following: #### Set your environment variables
```properties Fill in the `.env` section in Dockge with the following:
```properties [.env]
SCHEDULE= SCHEDULE=
WH_URL= WH_URL=
``` ```
@@ -165,11 +171,19 @@ WH_URL=
| `SCHEDULE` | Cron format | `0 0 6 * * *` (every day at 6 AM) | | `SCHEDULE` | Cron format | `0 0 6 * * *` (every day at 6 AM) |
| `WH_URL` | Your Discord webhook URL - append `/slack` at the end | `https://yourdiscordserver/webhook/slack` | | `WH_URL` | Your Discord webhook URL - append `/slack` at the end | `https://yourdiscordserver/webhook/slack` |
#### Enable Watchtower on other containers
To have Watchtower monitor your other containers, add this to their `compose.yml`: To have Watchtower monitor your other containers, add this to their `compose.yml`:
```yaml ```yaml [compose.yaml]
---
labels: labels:
- com.centurylinklabs.watchtower.enable=true - com.centurylinklabs.watchtower.enable=true
``` ```
Then restart the modified stacks. And that's it — you now have a solid base to start deploying the services you want! Then restart the modified stacks.
#### Done !
::
And that's it! You now have a solid base to start deploying the services you want!
@@ -1,22 +1,12 @@
--- ---
navigation: true
title: Wireguard title: Wireguard
description: Install and configure WireGuard VPN to securely access your homelab from anywhere and connect all your devices to your private network. description: Install and configure WireGuard VPN to securely access your homelab from anywhere and connect all your devices to your private network.
main:
fluid: false
--- ---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Wireguard
::alert{type="info"}
🎯 __Goals:__ :ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
- Install Wireguard
- Configure clients
- Access the secure network
::
## Introduction ## Introduction
---
Using a VPN allows remote access to a servers local resources without exposing them to the internet. Its a clean and secure way to access services like SSH without exposing the port publicly. With a VPN, you can securely connect to your network from anywhere and make devices on different networks communicate. Using a VPN allows remote access to a servers local resources without exposing them to the internet. Its a clean and secure way to access services like SSH without exposing the port publicly. With a VPN, you can securely connect to your network from anywhere and make devices on different networks communicate.
Here we will use [Wireguard](https://www.wireguard.com/), a secure and high-performance VPN server, using containers: Here we will use [Wireguard](https://www.wireguard.com/), a secure and high-performance VPN server, using containers:
@@ -49,36 +39,42 @@ You *can* allow VPN clients to share access to their local networks, but we won
So only VPN-connected devices can communicate with each other on the VPN, not with other local devices outside the VPN. So only VPN-connected devices can communicate with each other on the VPN, not with other local devices outside the VPN.
## Server Setup ## Server Setup
--- ::note{icon=""}
::alert{type="info"}
📋 **Pre-flight Checklist:** 📋 **Pre-flight Checklist:**
- Ensure port `51820 UDP` is free on your server and correctly forwarded from your router (`51820 UDP -> Server`). - Ensure port `51820 UDP` is free on your server and correctly forwarded from your router (`51820 UDP -> Server`).
- Ensure port `51821 TCP` is free for the web UI. - Ensure port `51821 TCP` is free for the web UI.
:: ::
::alert{type="warning"} ::warning
:::list{type="warning"}
- __Warning__: If your IP is not static, use a Dynamic DNS service ([DynDNS](https://en.wikipedia.org/wiki/Dynamic_DNS)). If your ISP uses [CGNAT](https://en.wikipedia.org/wiki/Carrier-grade_NAT), youll need to use an external VPS and connect your local server as a client. __Warning__: If your IP is not static, use a Dynamic DNS service ([DynDNS](https://en.wikipedia.org/wiki/Dynamic_DNS)). If your ISP uses [CGNAT](https://en.wikipedia.org/wiki/Carrier-grade_NAT), youll need to use an external VPS and connect your local server as a client.
:::
:: ::
### Folder Structure ### Folder Structure
```sh ::file-tree
root ---
└── docker tree:
└── wg-easy /:
├── config - srv:
│ └── etc_wireguard - docker:
├── compose.yaml - wg-easy:
└── .env - config:
``` - etc_wireguard/
- compose.yaml
- .env
---
::
::steps{level="3"}
### Deploy the stack
Open Dockge, click **Compose**, and name the stack `wg_easy`. Open Dockge, click **Compose**, and name the stack `wg_easy`.
Copy the following configuration: Copy the following configuration:
```yaml ```yaml [compose.yaml]
--- ---
services: services:
wg-easy: wg-easy:
@@ -118,12 +114,14 @@ networks:
- subnet: fdcc:ad94:bacf:61a3::/64 - subnet: fdcc:ad94:bacf:61a3::/64
``` ```
::alert{type="success"} ::tip{icon=""}
✨ **Tip:** ✨ **Tip:**
- You can customize WireGuard and web UI ports. - You can customize WireGuard and web UI ports.
- Add a Watchtower label for automatic updates: - Add a Watchtower label for automatic updates:
```yaml ```yaml [compose.yaml]
---
services: services:
wg-easy: wg-easy:
# ... # ...
@@ -134,76 +132,107 @@ services:
Deploy the stack and access the local web UI at `http://server-ip:51821`. Deploy the stack and access the local web UI at `http://server-ip:51821`.
::alert{type="danger"} ::caution
:::list{type="danger"}
- If the deployment fails, check your firewall rules. If the deployment fails, check your firewall rules.
:::
:: ::
### Create your account
Once connected, follow the web UI instructions to: Once connected, follow the web UI instructions to:
- Create your admin account and password. - Create your admin account and password.
- Set the host field (use your public IP or domain name). - Set the host field (use your public IP or domain name).
### Configure the tunnel
Then go to *Administrator → Admin Panel → Config*: Then go to *Administrator → Admin Panel → Config*:
- Change `Allowed IPs` from `0.0.0.0/24` to `10.8.0.0/24` for **split tunneling**. - Change `Allowed IPs` from `0.0.0.0/24` to `10.8.0.0/24` for **split tunneling**.
- Remove IPv6 (it often causes unnecessary issues). - Remove IPv6 (it often causes unnecessary issues).
### Done !
::
### Retrieve Configuration Files ### Retrieve Configuration Files
To configure clients: To configure clients:
1. Access the web UI: `http://server-ip:51821`
2. Create a new client ::steps{level="4"}
3. Edit the client and add `10.8.0.0/24` to `Server Allowed IPs` #### Access the web UI
4. (Optional) Set `Persistent Keep Alive` to `25` if its a permanently connected client
5. Save, download, and rename the file to `wg0.conf` (or `wg1.conf`, etc.) Go to `http://server-ip:51821`.
#### Create a new client
#### Edit the client
Add `10.8.0.0/24` to `Server Allowed IPs`.
#### (Optional) Set Persistent Keep Alive
Set it to `25` if its a permanently connected client.
#### Save and rename the file
Save, download, and rename the file to `wg0.conf` (or `wg1.conf`, etc.)
#### Done !
::
## Client Server Setup ## Client Server Setup
--- ::note
::alert{type="info"}
:::list{type="info"} We assume the client server runs Linux with Docker installed.
- We assume the client server runs Linux with Docker installed.
:::
:: ::
### Folder Structure ### Folder Structure
```sh ::file-tree
root ---
└── docker tree:
└── wireguard /:
└── config - srv:
│ └── wg_confs - docker:
└── compose.yaml - wireguard:
``` - config:
- wg_confs/
Create the folder: - compose.yaml
---
```sh
sudo mkdir -p /docker/wireguard/config/wg_confs
```
::alert{type="success"}
**Tip:** You can use [File Browser](/serveex/files/file-browser) instead of the terminal to edit and upload files.
:: ::
Create the `wg0.conf` file: ::steps{level="3"}
### Create the folder
```sh ```bash [Terminal]
sudo vi /docker/wireguard/config/wg_confs/wg0.conf sudo mkdir -p /srv/docker/wireguard/config/wg_confs
``` ```
Enter insert mode (`i`), paste the downloaded configuration, then save (`Esc``:x`). ::tip{icon="" to="/serveex/files/file-browser-quantum"}
**Tip:** You can use **File Browser Quantum** instead of the terminal to edit and upload files.
::
::alert{type="success"} ### Create the wg0.conf file
```bash [Terminal]
sudo nano /srv/docker/wireguard/config/wg_confs/wg0.conf
```
Paste the downloaded configuration, then save with :kbd{value="Ctrl+O"}, :kbd{value="Enter"}, and exit with :kbd{value="Ctrl+X"}.
::tip{icon=""}
**Alternative method:** Transfer the file via SFTP and move it: **Alternative method:** Transfer the file via SFTP and move it:
```sh ```bash [Terminal]
sudo cp ~/wg0.conf /docker/wireguard/config/wg_confs sudo cp ~/wg0.conf /srv/docker/wireguard/config/wg_confs
``` ```
:: ::
Create the `compose.yaml` file in `/docker/wireguard`: ### Create the compose file
```yaml Create the `compose.yaml` file in `/srv/docker/wireguard`:
```yaml [compose.yaml]
---
services: services:
wireguard: wireguard:
image: lscr.io/linuxserver/wireguard:latest image: lscr.io/linuxserver/wireguard:latest
@@ -215,33 +244,35 @@ services:
environment: environment:
- TZ=Europe/Paris - TZ=Europe/Paris
volumes: volumes:
- /docker/wireguard/config:/config - /srv/docker/wireguard/config:/config
- /lib/modules:/lib/modules - /lib/modules:/lib/modules
restart: unless-stopped restart: unless-stopped
``` ```
Start the container: ### Start the container
```sh
cd /docker/wireguard ```bash [Terminal]
cd /srv/docker/wireguard
sudo docker compose up -d sudo docker compose up -d
``` ```
::alert{type="info"} ### Done !
:::list{type="info"} ::
- Repeat this setup for each client.
::: ::note
Repeat this setup for each client.
:: ::
## Other Devices ## Other Devices
---
- **Mobile:** Install WireGuard and scan the QR code via the web UI (`http://server-ip:51821`) - **Mobile:** Install WireGuard and scan the QR code via the web UI (`http://server-ip:51821`)
- **Desktop:** Install the WireGuard client and import the downloaded config file. - **Desktop:** Install the WireGuard client and import the downloaded config file.
::alert{type="warning"} ::warning
:::list{type="warning"}
- **Note:** If the client machine is on the same local network as the server, edit the `wg0.conf` file to use the local server IP: **Note:** If the client machine is on the same local network as the server, edit the `wg0.conf` file to use the local server IP:
`Endpoint = server-local-ip:51820` `Endpoint = server-local-ip:51820`
:::
:: ::
And heres the final setup overview: And heres the final setup overview:
@@ -1,28 +1,16 @@
--- ---
navigation: true
title: SWAG title: SWAG
description: Set up SWAG as a reverse proxy with automatic SSL, expose your services securely, and configure geo-blocking on your homelab. description: Set up SWAG as a reverse proxy with automatic SSL, expose your services securely, and configure geo-blocking on your homelab.
main:
fluid: false
--- ---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# SWAG
::alert{type="info"}
🎯 __Objectives:__ :ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
- Install Swag
- Enable SSL
- Access the dashboard
- Configure regional blocking
- Expose Dockge
::
[Swag](https://docs.linuxserver.io/general/swag/) is the core of this homelab. Its a powerful reverse proxy that allows you to expose services on the internet using domain names, handling SSL certificate issuance (for encrypted connections), request routing, and access security (via HTTP auth or SSO like Authelia or Authentik). All the necessary documentation is [available here](https://docs.linuxserver.io/general/swag). [Swag](https://docs.linuxserver.io/general/swag/) is the core of this homelab. Its a powerful reverse proxy that allows you to expose services on the internet using domain names, handling SSL certificate issuance (for encrypted connections), request routing, and access security (via HTTP auth or SSO like Authelia or Authentik). All the necessary documentation is [available here](https://docs.linuxserver.io/general/swag).
::alert{type="warning"} ::warning{to="/serveex/core/wireguard"}
:::list{type="warning"}
- SWAG is only useful for exposing your services to the interneti.e., accessing them via a public URL like `https://service.mydomain.com`. If you dont want to expose your services and prefer to always use a VPN to connect remotely, you can go [here instead](/serveex/security/wireguard). SWAG is only useful for exposing your services to the internet, i.e. accessing them via a public URL like `https://service.mydomain.com`. If you dont want to expose your services and prefer to always use a VPN to connect remotely, you can go **here instead**.
:::
:: ::
Below is an example exposing Dockge. We will install SWAG along with the dbip mod for geolocation-based blocking, and the dashboard mod for managing swag, fail2ban, and geolocation. Below is an example exposing Dockge. We will install SWAG along with the dbip mod for geolocation-based blocking, and the dashboard mod for managing swag, fail2ban, and geolocation.
@@ -32,35 +20,39 @@ Below is an example exposing Dockge. We will install SWAG along with the dbip mo
![Picture](/img/serveex/reverse-proxy.svg) ![Picture](/img/serveex/reverse-proxy.svg)
## Installation ## Installation
---
::alert{type="info" icon="exclamation-circle"} ::note
:::list{type="info"}
- This tutorial assumes you have a domain name pointing to your server, and that your router has a NAT rule forwarding port `443` to your server's IP and port `443`. The example domain will be `mydomain.com`. This tutorial assumes you have a domain name pointing to your server, and that your router has a NAT rule forwarding port `443` to your server's IP and port `443`. The example domain will be `mydomain.com`.
:::
:: ::
File structure to be modified: ::file-tree
---
label: File structure to modify
tree:
/:
- srv:
- docker:
- swag:
- config:
- dns-conf:
- ovh.ini
- nginx:
- dbip.conf
- nginx.conf
- proxy-confs:
- dockge.subdomain.conf
- compose.yml
- .env
---
::
```sh ::steps{level="3"}
root ### Deploy the stack
└── docker
└── swag
├── config
│ ├── dns-conf
│ │ └── ovh.ini
│ └── nginx
│ ├── dbip.conf
│ ├── nginx.conf
│ └── proxy-confs
│ └── dockge.subdomain.conf
├── compose.yml
└── .env
```
Open Dockge in your browser, click on `compose`, name the stack `swag`, and copy the following config: Open Dockge in your browser, click on `compose`, name the stack `swag`, and copy the following config:
```yaml ```yaml [compose.yaml]
--- ---
services: services:
swag: swag:
@@ -80,7 +72,7 @@ services:
- EMAIL=${EMAIL} - EMAIL=${EMAIL}
- DOCKER_MODS=linuxserver/mods:swag-dbip|linuxserver/mods:swag-dashboard|linuxserver/mods:swag-auto-reload - DOCKER_MODS=linuxserver/mods:swag-dbip|linuxserver/mods:swag-dashboard|linuxserver/mods:swag-auto-reload
volumes: volumes:
- /docker/swag/config:/config - /srv/docker/swag/config:/config
ports: ports:
- 80:80 - 80:80
- 443:443 - 443:443
@@ -94,11 +86,12 @@ networks:
name: swag_default name: swag_default
``` ```
::alert{type="success"} ::tip{icon=""}
✨ __Tip:__ ✨ __Tip:__
Add the watchtower label to each container to enable automatic updates Add the watchtower label to each container to enable automatic updates
```yaml ```yaml [compose.yaml]
---
services: services:
swag: swag:
#... #...
@@ -107,9 +100,11 @@ services:
``` ```
:: ::
### Set your environment variables
Then in the `.env` file: Then in the `.env` file:
```properties ```properties [.env]
DOMAIN= DOMAIN=
DOMAINS= DOMAINS=
EMAIL= EMAIL=
@@ -123,24 +118,26 @@ Fill out the variables as follows:
| `DOMAIN` | Your domain (covers all subdomains too) | `mydomain.com` | | `DOMAIN` | Your domain (covers all subdomains too) | `mydomain.com` |
| `DOMAINS` | Any additional domains | `myseconddomain.com` | | `DOMAINS` | Any additional domains | `myseconddomain.com` |
| `EMAIL` | Your email for generating the certificate | `[email protected]` | | `EMAIL` | Your email for generating the certificate | `[email protected]` |
| `PLUGIN` | Plugin for certificate generationdepends on your [DNS provider](https://docs.linuxserver.io/general/swag/) | `ovh`<br>`cloudflare` | | `PLUGIN` | Plugin for certificate generation, depends on your [DNS provider](https://docs.linuxserver.io/general/swag/) | `ovh`<br>`cloudflare` |
Assuming your DNS zone is managed by OVH, deploy the stack once. The logs will show a failure in creating the SSL certificate due to a missing `ovh.ini` configuration. Stop the stack. ### Configure the OVH DNS plugin
Assuming your DNS zone is managed by OVH (if not, please check for your [provider](https://github.com/linuxserver/docker-swag/tree/master/root/defaults/dns-conf)), deploy the stack once. The logs will show a failure in creating the SSL certificate due to a missing `ovh.ini` configuration. Stop the stack.
In CLI, go to the dns-conf folder and edit the `ovh.ini` file: In CLI, go to the dns-conf folder and edit the `ovh.ini` file:
::alert{type="success"} ::tip{icon="" to="/serveex/files/file-browser-quantum"}
✨ __Tip for terminal-shy users:__ ✨ __Tip for terminal-shy users:__
You can use [File Browser](/serveex/files/file-browser) to browse and edit files instead of using terminal commands. You can use **File Browser Quantum** to browse and edit files instead of using terminal commands.
:: ::
```sh ```bash [Terminal]
sudo vi /docker/swag/config/dns-conf/ovh.ini sudo nano /srv/docker/swag/config/dns-conf/ovh.ini
``` ```
You should see: You should see:
```properties ```properties [ovh.ini]
# Instructions: https://github.com/certbot/certbot/blob/master/certbot-dns-ovh/certbot_dns_ovh/__init__.py#L20 # Instructions: https://github.com/certbot/certbot/blob/master/certbot-dns-ovh/certbot_dns_ovh/__init__.py#L20
# Replace with your values # Replace with your values
dns_ovh_endpoint = ovh-eu dns_ovh_endpoint = ovh-eu
@@ -158,49 +155,54 @@ Set the following permissions:
* `POST /domain/zone/*` * `POST /domain/zone/*`
* `DELETE /domain/zone/*` * `DELETE /domain/zone/*`
Note the 3 keys temporarily and enter them in `ovh.ini`. (In vim, press `i` to edit, `Esc` when done, `:x` to save and exit) Note the 3 keys temporarily and enter them in `ovh.ini`. (In nano, just start typing, then :kbd{value="Ctrl+O"}, :kbd{value="Enter"}, :kbd{value="Ctrl+X"} to save and exit)
Save and exit the file. Save with :kbd{value="Ctrl+O"}, then :kbd{value="Enter"}, and exit with :kbd{value="Ctrl+X"}.
### Enable DBIP in nginx.conf
Now configure swag to access DBIP, the geolocation-based access control module. Open the `nginx.conf` file: Now configure swag to access DBIP, the geolocation-based access control module. Open the `nginx.conf` file:
```sh ```bash [Terminal]
sudo vi /docker/swag/config/nginx/nginx.conf sudo nano /srv/docker/swag/config/nginx/nginx.conf
``` ```
Add the following line below the `http` section: Add the following line below the `http` section:
```nginx ```nginx [nginx.conf]
include /config/nginx/dbip.conf; include /config/nginx/dbip.conf;
``` ```
Restart the stack in Dockge. This time, the SSL certificate should be successfully generated! Check the logs to confirm the server is ready. Restart the stack in Dockge. This time, the SSL certificate should be successfully generated! Check the logs to confirm the server is ready.
### Done !
::
## Dashboard ## Dashboard
---
Access the dashboard locally by going to `http://yourserverip:81` Access the dashboard locally by going to `http://yourserverip:81`
On the left, you'll see a list of currently "proxied" services (none yet). On the right, the list of banned IPs. Below, various indicators. For more details, [click here](https://www.linuxserver.io/blog/introducing-swag-dashboard). On the left, you'll see a list of currently "proxied" services (none yet). On the right, the list of banned IPs. Below, various indicators. For more details, [click here](https://www.linuxserver.io/blog/introducing-swag-dashboard).
![picture](https://www.linuxserver.io/user/pages/03.blog/introducing-swag-dashboard/example.png) ![picture](https://www.linuxserver.io/user/pages/03.blog/introducing-swag-dashboard/example.png)
## DBIP ## DBIP
--- DBIP allows you to block connections based on countries. It relies on the configuration file named `dbip.conf` located in `/srv/docker/swag/config/nginx`. [More info here](https://virtualize.link/secure/).
DBIP allows you to block connections based on countries. It relies on the configuration file named `dbip.conf` located in `/docker/swag/config/nginx`. [More info here](https://virtualize.link/secure/).
In this example, well configure it to block a list of countries known to be the source of most malicious traffic. Well also configure a variable to allow internal server traffic, your boxs local network, and a potential VPN in the 10.x.x.x range to access your services but not the open internet. In this example, well configure it to block a list of countries known to be the source of most malicious traffic. Well also configure a variable to allow internal server traffic, your boxs local network, and a potential VPN in the 10.x.x.x range to access your services, but not the open internet.
This configuration can be enabled or disabled per service (see the Dockge example below). This configuration can be enabled or disabled per service (see the Dockge example below).
Open `dbip.conf`: ::steps{level="3"}
### Open dbip.conf
```sh ```bash [Terminal]
sudo vi /docker/swag/config/nginx/dbip.conf sudo nano /srv/docker/swag/config/nginx/dbip.conf
``` ```
Make your changes ([see documentation](https://github.com/linuxserver/docker-mods/tree/swag-dbip)), or use the following example: ### Make your changes
```nginx Refer to the [documentation](https://github.com/linuxserver/docker-mods/tree/swag-dbip), or use the following example:
```nginx [dbip.conf]
geoip2 /config/geoip2db/dbip-country-lite.mmdb { geoip2 /config/geoip2db/dbip-country-lite.mmdb {
auto_reload 1w; auto_reload 1w;
$geoip2_data_continent_code continent code; $geoip2_data_continent_code continent code;
@@ -244,48 +246,65 @@ geo $lan-ip {
} }
``` ```
Save and close the file. Restart the stack. ### Save and restart
Save and close the file, then restart the stack.
### Done !
::
In the domain config files (see next section), you can enable or disable the whitelist or blacklist ([see documentation here](https://www.forum-nas.fr/threads/tuto-installer-swag-en-docker-reverse-proxy.15057/)). In our case, the whitelist allows only French requests. The blacklist blocks only the listed countries. We'll use the blacklist, like so: In the domain config files (see next section), you can enable or disable the whitelist or blacklist ([see documentation here](https://www.forum-nas.fr/threads/tuto-installer-swag-en-docker-reverse-proxy.15057/)). In our case, the whitelist allows only French requests. The blacklist blocks only the listed countries. We'll use the blacklist, like so:
```nginx ```nginx [some-app.subdomain.conf]{11}
server { server {
listen 443 ssl; listen 443 ssl;
listen [::]:443 ssl; listen [::]:443 ssl;
server_name some-app.*; server_name some-app.*;
include /config/nginx/ssl.conf; include /config/nginx/ssl.conf;
client_max_body_size 0; client_max_body_size 0;
if ($geo-blacklist = no) { return 404; } if ($geo-blacklist = no) { return 404; }
location / { location / {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app some-app;
set $upstream_port 1234;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
``` ```
## Exposing Dockge ## (Optional) Exposing Dockge
--- ::note{icon=""}
::alert{type="info"}
📋 __Prerequisite:__ <br/><br/> 📋 __Prerequisite:__ <br/><br/>
We assume that you have created a subdomain like `dockge.mydomain.com` in your [DNS zone](/general/networking/dns), with a `CNAME` pointing to `mydomain.com` and — unless you're using [Cloudflare Zero Trust](/serveex/security/cloudflare) — that you've forwarded port `443` from your router to the server's `443` in [your NAT rules](/general/networking/nat). We assume that you have created a subdomain like `dockge.mydomain.com` in your [DNS zone](/general/networking/dns), with a `CNAME` pointing to `mydomain.com`. Unless you're using [Cloudflare Zero Trust](/serveex/security/cloudflare), we also assume you've forwarded port `443` from your router to the server's `443` in [your NAT rules](/general/networking/nat).
:: ::
Now it's time to expose Dockge on the internet so you can access and manage your containers remotely. We assume you've set up the subdomain `dockge.mydomain.com` with a `CNAME` pointing to `mydomain.com`. Now it's time to expose Dockge on the internet so you can access and manage your containers remotely. We assume you've set up the subdomain `dockge.mydomain.com` with a `CNAME` pointing to `mydomain.com`.
::alert{type="warning"} ::warning
:::list{type="warning"}
- Dockge does not support multi-factor authentication. Exposing it online could compromise all connected machines. Only do this if you're using an MFA solution like [Authentik](/serveex/security/authentik/). Otherwise, dont expose it with SWAG — use a VPN like [Wireguard](/serveex/security/wireguard) instead. Dockge does not support multi-factor authentication. Exposing it online could compromise all connected machines. Only do this if you're using an MFA solution like [TinyAuth](/serveex/security/tinyauth) or [Authentik](/serveex/advanced/authentik/). Otherwise, dont expose it with SWAG. Use a VPN like [Wireguard](/serveex/core/wireguard) instead.
:::
:: ::
::steps{level="3"}
### Create the subdomain.conf file
Open the `dockge.subdomain.conf` file: Open the `dockge.subdomain.conf` file:
```sh ```bash [Terminal]
sudo vi /docker/swag/config/nginx/proxy-confs/dockge.subdomain.conf sudo nano /srv/docker/swag/config/nginx/proxy-confs/dockge.subdomain.conf
``` ```
Configure it like this: Configure it like this:
```nginx ```nginx [dockge.subdomain.conf]
## Version 2023/12/19 ## Version 2023/12/19
server { server {
@@ -327,20 +346,23 @@ server {
Save and exit. The configuration will update within a few seconds. Save and exit. The configuration will update within a few seconds.
::alert{type="info"} ::note
:::list{type="info"}
- By default, SWAG doesnt recognize the name "dockge". Youll need to add Dockges network to SWAGs `compose.yml`. By default, SWAG doesnt recognize the name "dockge". Youll need to add Dockges network to SWAGs `compose.yml`.
:::
:: ::
### Add Dockge's network to SWAG
Go to the SWAG stack, click `edit`, and modify the config file like this (note the `networks` section): Go to the SWAG stack, click `edit`, and modify the config file like this (note the `networks` section):
```yaml ```yaml [compose.yaml]
---
services: services:
swag: swag:
container_name: #... container_name: #...
# ... # ...
networks: # Link the container to the custom network networks: # Link the container to the custom network
- dockge # Network name as defined in the stack - dockge # Network name as defined in the stack
networks: # Define the custom network networks: # Define the custom network
@@ -350,29 +372,31 @@ networks: # Define the custom network
external: true external: true
``` ```
::alert{type="info"} ::note
:::list{type="info"}
- We assume the Dockge network is named `dockge_default`. You can verify the setup works by checking the SWAG dashboard at `http://yourserverip:81`. We assume the Dockge network is named `dockge_default`. You can verify the setup works by checking the SWAG dashboard at `http://yourserverip:81`.
:::
:: ::
Redeploy the SWAG stack. Redeploy the SWAG stack.
Wait a moment, then visit `https://dockge.mydomain.com` in your browser — you should be redirected to Dockge. You can also check the service status from the dashboard (`http://yourserverip:81` on your local network). ### Visit your new subdomain
Wait a moment, then visit `https://dockge.mydomain.com` in your browser. You should be redirected to Dockge. You can also check the service status from the dashboard (`http://yourserverip:81` on your local network).
### Done !
::
## Exposing Another Service with SWAG ## Exposing Another Service with SWAG
---
SWAG includes templates for most known services, named `servicename.subdomain.conf.sample`. Just create the subdomain in your registrar's DNS zone (like OVH), point it to your main domain via a CNAME, then copy and rename the sample file: SWAG includes templates for most known services, named `servicename.subdomain.conf.sample`. Just create the subdomain in your registrar's DNS zone (like OVH), point it to your main domain via a CNAME, then copy and rename the sample file:
```sh ```bash [Terminal]
cd /docker/swag/config/proxy-confs cd /srv/docker/swag/config/proxy-confs
sudo cp servicename.subdomain.conf.sample servicename.subdomain.conf sudo cp servicename.subdomain.conf.sample servicename.subdomain.conf
``` ```
::alert{type="danger"} ::caution
:::list{type="danger"}
- __If the subdomain is not redirected properly__ __If the subdomain is not redirected properly__
:::
- Open the file and verify the container name in `set $upstream_app containername;`{lang=nginx} - Open the file and verify the container name in `set $upstream_app containername;`{lang=nginx}
- Make sure you added the container's network in SWAGs `compose.yml` - Make sure you added the container's network in SWAGs `compose.yml`
:: ::
@@ -0,0 +1,2 @@
title: Security
icon: i-lucide-shield
@@ -1,28 +1,16 @@
--- ---
navigation: true
title: Cloudflare Zero Trust title: Cloudflare Zero Trust
description: Use Cloudflare Tunnels and Zero Trust to expose homelab services without opening ports configure SWAG and manage multiple tunnels. description: Use Cloudflare Tunnels and Zero Trust to expose homelab services without opening ports, configure SWAG and manage multiple tunnels.
main:
fluid: false
--- ---
:ellipsis{left=0px width=40rem top=10rem blur=140px}
# Cloudflare Zero Trust
::alert{type="info"}
🎯 __Goals:__
- Understand the concept of Cloudflare Tunnels
- Configure your Cloudflare account
- Configure SWAG
- Manage multiple tunnels
::
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
![cloudfare_tunnels](/img/serveex/cloudflared.svg) ![cloudfare_tunnels](/img/serveex/cloudflared.svg)
## Introduction ## Introduction
---
The _Zero Trust_ architecture is the practice of designing systems based on the principle of __"never trust, always verify"__, as opposed to the traditional principle of __"trust, but verify"__. This concept has become increasingly popular recently due to the growing number of attacks targeting user data. Its a broad concept, but well focus on how to apply _Zero Trust_ to the web services we host. The _Zero Trust_ architecture is the practice of designing systems based on the principle of __"never trust, always verify"__, as opposed to the traditional principle of __"trust, but verify"__. This concept has become increasingly popular recently due to the growing number of attacks targeting user data. Its a broad concept, but well focus on how to apply _Zero Trust_ to the web services we host.
_Cloudflare tunnels_ offer a simple way to implement _Zero Trust_, using [SWAG](/serveex/core/swag) and [Authentik](/serveex/security/authentik). _Cloudflare tunnels_ offer a simple way to implement _Zero Trust_, using [SWAG](/serveex/core/swag) and [Authentik](/serveex/advanced/authentik).
Simply put, Cloudflare Tunnels allow you to: Simply put, Cloudflare Tunnels allow you to:
@@ -34,17 +22,14 @@ Simply put, Cloudflare Tunnels allow you to:
Here well explain how to integrate SWAG with Cloudflare tunnels. Here well explain how to integrate SWAG with Cloudflare tunnels.
::alert{type="warning"} ::warning
:::list{type="warning"} __Warning:__
- __Warning:__
:::
- Do not use Cloudflare tunnels to expose a mail server - Do not use Cloudflare tunnels to expose a mail server
- Do not use Cloudflare tunnels to expose a video service like Plex (if you followed [this guide](/serveex/media/plex), Plex is not exposed, so its fine) - Do not use Cloudflare tunnels to expose a video service like Jellyfin. Unlike Plex, [Jellyfin has no cloud relay](/serveex/media/jellyfin) and is exposed directly through SWAG in this guide, so make sure it stays behind plain port forwarding rather than a Cloudflare tunnel
- Do not use Cloudflare tunnels for the BitTorrent protocol (if you followed [this guide](/serveex/media/qbittorrent), everything is fine) - Do not use Cloudflare tunnels for the BitTorrent protocol (if you followed [this guide](/serveex/media/qbittorrent), everything is fine)
:: ::
## Cloudflare Configuration ## Cloudflare Configuration
---
### DNS Zone ### DNS Zone
First, you need to set Cloudflare as your [DNS zone](/general/networking/dns) manager. If you bought your domain from Cloudflare, thats already done. Otherwise, check with your registrar how to add external DNS servers. Cloudflare provides [step-by-step documentation](https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/) on how to configure a DNS Zone, whether your domain is external or registered with Cloudflare. First, you need to set Cloudflare as your [DNS zone](/general/networking/dns) manager. If you bought your domain from Cloudflare, thats already done. Otherwise, check with your registrar how to add external DNS servers. Cloudflare provides [step-by-step documentation](https://developers.cloudflare.com/dns/zone-setups/full-setup/setup/) on how to configure a DNS Zone, whether your domain is external or registered with Cloudflare.
@@ -53,7 +38,7 @@ If you only have one server to protect behind Cloudflare, you can delete all exi
If you have subdomains pointing to other servers, you can still define them in the DNS zone using A records. If you have subdomains pointing to other servers, you can still define them in the DNS zone using A records.
If you have several servers and tunnels under one domain, [see here](http://192.168.7.80:8005/serveex/cloudflare/#gerer-plusieurs-tunnels-pour-plusieurs-serveurs). If you have several servers and tunnels under one domain, [see here](#managing-multiple-tunnels-for-multiple-servers).
### API Key ### API Key
@@ -71,16 +56,14 @@ Once created, your token will only be shown once. Save it securely, as it cannot
### Cloudflare Zero Trust ### Cloudflare Zero Trust
You must register for _Cloudflare Teams_ to access the _Zero Trust_ dashboard that manages tunnels and access policies. This is a premium service, but theres a free plan for up to 50 usersperfect for a home lab. Keep in mind that a valid credit card is required to register, but the free plan incurs no charges. You must register for _Cloudflare Teams_ to access the _Zero Trust_ dashboard that manages tunnels and access policies. This is a premium service, but theres a free plan for up to 50 users, perfect for a home lab. Keep in mind that a valid credit card is required to register, but the free plan incurs no charges.
Register [via this link](https://dash.teams.cloudflare.com/). Register [via this link](https://dash.teams.cloudflare.com/).
## SWAG Configuration ## SWAG Configuration
--- ::note
::alert{type="info"}
:::list{type="info"} This guide assumes you own `mondomaine.fr` and that its DNS is correctly pointing to Cloudflare, as described above.
- This guide assumes you own `mondomaine.fr` and that its DNS is correctly pointing to Cloudflare, as described above.
:::
:: ::
SWAG supports two Docker Mods: SWAG supports two Docker Mods:
@@ -90,57 +73,60 @@ SWAG supports two Docker Mods:
These two mods, merged into the SWAG container, require some configuration. These two mods, merged into the SWAG container, require some configuration.
### Tunnel Configuration ::steps{level="3"}
### Configure the tunnel
Create a file `tunnelconfig.yml` to reference in your SWAG `compose.yaml`. Create a file `tunnelconfig.yml` to reference in your SWAG `compose.yaml`.
::alert{type="success"} ::tip{icon="" to="/serveex/files/file-browser-quantum"}
__Tip:__ Use [File Browser](/serveex/files/file-browser) to navigate and edit files instead of using the terminal. __Tip:__ Use **File Browser Quantum** to navigate and edit files instead of using the terminal.
:: ::
```sh ```bash [Terminal]
sudo vi /docker/swag/config/tunnelconfig.yml sudo nano /srv/docker/swag/config/tunnelconfig.yml
``` ```
Press `i` to enter insert mode and paste: Paste:
```yaml ```yaml [tunnelconfig.yml]
ingress: ingress:
- hostname: mondomaine.fr - hostname: mondomaine.fr
service: https://mondomaine.fr service: https://mondomaine.fr
- hostname: "*.mondomaine.fr" - hostname: "*.mondomaine.fr"
service: https://mondomaine.fr service: https://mondomaine.fr
- service: http_status:404 - service: http_status:404
``` ```
Press `Esc`, then save and exit with `:x` and `Enter`. Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
### Cloudflare Real IP Configuration ### Configure Cloudflare Real IP
Now configure _Cloudflare Real IP_. Now configure _Cloudflare Real IP_.
Open the `nginx.conf` file: Open the `nginx.conf` file:
```sh ```bash [Terminal]
sudo vi /docker/swag/config/nginx/nginx.conf sudo nano /srv/docker/swag/config/nginx/nginx.conf
``` ```
Press `i` and add the following at the end of the `http` section: Add the following at the end of the `http` section:
```nginx ```nginx [nginx.conf]
real_ip_header X-Forwarded-For; real_ip_header X-Forwarded-For;
real_ip_recursive on; real_ip_recursive on;
include /config/nginx/cf_real-ip.conf; include /config/nginx/cf_real-ip.conf;
set_real_ip_from 127.0.0.1; set_real_ip_from 127.0.0.1;
``` ```
Save and exit with `:x`. Save with :kbd{value="Ctrl+O"}, then :kbd{value="Enter"}, and exit with :kbd{value="Ctrl+X"}.
### Docker Compose ### Deploy the SWAG stack
In Dockge, edit your SWAG stack with this: In Dockge, edit your SWAG stack with this:
```yaml ```yaml [compose.yaml]
--- ---
services: services:
swag: swag:
@@ -171,15 +157,16 @@ services:
ports: ports:
- 81:81 - 81:81
volumes: volumes:
- /docker/swag/config:/config - /srv/docker/swag/config:/config
- /docker/swag/config/fail2ban/fail2ban.sqlite3:/dashboard/fail2ban.sqlite3:ro - /srv/docker/swag/config/fail2ban/fail2ban.sqlite3:/dashboard/fail2ban.sqlite3:ro
restart: unless-stopped restart: unless-stopped
``` ```
::alert{type="success"} ::tip{icon=""}
__Tip:__ Add a Watchtower label to automate updates: __Tip:__ Add a Watchtower label to automate updates:
```yaml ```yaml [compose.yaml]
---
labels: labels:
- com.centurylinklabs.watchtower.enable=true - com.centurylinklabs.watchtower.enable=true
``` ```
@@ -187,7 +174,7 @@ labels:
Fill in your `.env` file: Fill in your `.env` file:
```properties ```properties [.env]
PUID= PUID=
PGID= PGID=
DOMAIN= DOMAIN=
@@ -213,36 +200,44 @@ TUNNEL_PW=
| `TUNNEL_NAME` | Tunnel name | `my_tunnel` | | `TUNNEL_NAME` | Tunnel name | `my_tunnel` |
| `TUNNEL_PW` | Strong, random password | `iSzKRmP4VbnlsMvdSdgBEJiJi` | | `TUNNEL_PW` | Strong, random password | `iSzKRmP4VbnlsMvdSdgBEJiJi` |
Once done, deploy the stack. Check the logsyou should reach `server ready`. Once done, deploy the stack. Check the logs: you should reach `server ready`.
Then confirm your tunnel appears under _Networks > Tunnels_ in [Cloudflare Zero Trust](https://one.dash.cloudflare.com/). By default, all subdomains will be routed through the tunnelno need to define them [in your DNS zone](/general/networking/dns). Then confirm your tunnel appears under _Networks > Tunnels_ in [Cloudflare Zero Trust](https://one.dash.cloudflare.com/). By default, all subdomains will be routed through the tunnel, no need to define them [in your DNS zone](/general/networking/dns).
::alert{type="success"} ::tip{icon="" to="/general/networking/dns"}
__Tip:__ If you want to expose a service without a tunnel, just define an A record [in your DNS zone](/general/networking/dns). If resolution fails, disable the proxy function for that recorde.g., for `sub.mondomaine.fr`. __Tip:__ If you want to expose a service without a tunnel, just define an A record **in your DNS zone**. If resolution fails, disable the proxy function for that record, e.g. for `sub.mondomaine.fr`.
![dns](/img/serveex/cf-dns.png) ![dns](/img/serveex/cf-dns.png)
:: ::
### Done !
::
## Managing Multiple Tunnels for Multiple Servers ## Managing Multiple Tunnels for Multiple Servers
--- By default, all subdomains of your domain are routed through the single tunnel. But if you have a second server, just change the tunnel name in that SWAG instance, and redirect subdomains to the correct tunnel in your DNS zone.
By default, all subdomains of your domain are routed through the single tunnel. But if you have a second server, just change the tunnel name in that SWAG instance.
In your DNS zone, redirect subdomains to the correct tunnel. ::steps{level="3"}
### Change the tunnel name
Go to _Networks > Tunnels_ in [Cloudflare Zero Trust](https://one.dash.cloudflare.com/). In the second server's SWAG stack, set a different `TUNNEL_NAME` in the `.env` file, then redeploy.
Note the tunnel IDs: ### Find the tunnel IDs
Go to _Networks > Tunnels_ in [Cloudflare Zero Trust](https://one.dash.cloudflare.com/) and note the tunnel IDs:
![tunnels_id](/img/serveex/cf-tunnels-id.png) ![tunnels_id](/img/serveex/cf-tunnels-id.png)
Then in the [Cloudflare DNS dashboard](https://dash.cloudflare.com/), click your domain name. ### Add CNAME records
Click `Add Record` and add these two CNAME records (include `.cfargotunnel.com`): In the [Cloudflare DNS dashboard](https://dash.cloudflare.com/), click your domain name, then `Add Record` and add these two CNAME records (include `.cfargotunnel.com`):
| Type | Name | Target | | Type | Name | Target |
|---------|--------------|----------------------------------------| |---------|--------------|----------------------------------------|
| `CNAME` | `subdomain1` | `yourtunnelid1.cfargotunnel.com` | | `CNAME` | `subdomain1` | `yourtunnelid1.cfargotunnel.com` |
| `CNAME` | `subdomain2` | `yourtunnelid2.cfargotunnel.com` | | `CNAME` | `subdomain2` | `yourtunnelid2.cfargotunnel.com` |
### Done !
::
If you have many subdomains, point them to the above reference subdomains. If you have many subdomains, point them to the above reference subdomains.
This way, if a tunnel ID changes, you only update one DNS record. This way, if a tunnel ID changes, you only update one DNS record.
@@ -0,0 +1,353 @@
---
title: TinyAuth
description: Install TinyAuth, a lightweight forward-auth proxy, and pair it with Pocket ID to add SSO login in front of your self-hosted apps. Protect your app behind Swag with forward-auth.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
[TinyAuth](https://tinyauth.app) is a small forward-auth proxy: a single login page that Swag can insert in front of any app before letting a request through, checking whether a visitor is authenticated before forwarding them on.
![tinyauth](/img/serveex/tinyauth.png)
It supports a simple local username/password login out of the box, which is what we'll set up here. It can also delegate login to an external OIDC provider like [Pocket ID](/serveex/security/pocket-id) instead, so anyone visiting a protected app authenticates with a passkey via Pocket ID and then gets forwarded through: install Pocket ID afterwards and follow [its guide](/serveex/security/pocket-id#connecting-pocket-id-to-tinyauth) to connect the two.
- [TinyAuth documentation](https://tinyauth.app/docs)
- [TinyAuth on GitHub](https://github.com/tinyauthapp/tinyauth)
## Installation
::file-tree
---
tree:
/:
- srv:
- docker:
- tinyauth:
- compose.yaml
- .env
- data/
---
::
::steps{level="3"}
### Create the data folder
```bash [Terminal]
sudo mkdir -p /srv/docker/tinyauth/data
```
### Generate a password hash
```bash [Terminal]
sudo docker run -i -t --rm ghcr.io/tinyauthapp/tinyauth:v5 user create --interactive
```
::note
Enable "Format for Docker" when prompted, so the generated hash is already escaped for use in a `.env` file.
::
### Deploy the stack
Open Dockge, click `compose`, name the stack `tinyauth`, and add the following config:
```yaml [compose.yaml]
---
services:
tinyauth:
image: ghcr.io/tinyauthapp/tinyauth:v5
container_name: tinyauth
restart: unless-stopped
env_file:
- .env
volumes:
- /srv/docker/tinyauth/data:/data
ports:
- 3000:3000
```
::tip{icon=""}
✨ Add the Watchtower label to automate updates:
```yaml [compose.yaml]
---
services:
tinyauth:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
```
::
### Set your environment variables
Fill in the `.env` file:
```properties [.env]
TINYAUTH_APPURL=https://tinyauth.mydomain.com
TINYAUTH_AUTH_USERS=
```
| Variable | Value | Example |
|----------|-------|---------|
| `TINYAUTH_APPURL`{lang=properties} | The public URL you'll use to reach TinyAuth (see exposure below) | `https://tinyauth.mydomain.com` |
| `TINYAUTH_AUTH_USERS`{lang=properties} | The hash generated above | `user:$$2a$$10$$UdLYoJ5lgPsC0RKq...` |
Deploy the stack. The local interface is available at `http://yourserverip:3000`.
### Done !
::
## Enabling Two-Factor Authentication
TinyAuth can require a TOTP code from an authenticator app (Google Authenticator, Aegis...) alongside the local password, per user. This is a property of the user entry itself, not a toggle in the web UI.
::steps{level="3"}
### Generate a TOTP secret
```bash [Terminal]
sudo docker run -i -t --rm ghcr.io/tinyauthapp/tinyauth:v5 totp generate --interactive
```
Enter the `username:hash` pair you generated during installation. TinyAuth prints a QR code to scan with your authenticator app, then outputs the updated login string as `username:hash:secret`.
::note
Both `docker run` and `docker exec` need the `-it` flags here: the command is interactive and renders the QR code in the terminal, which needs a TTY (and a wide enough window) to display correctly.
::
### Update your environment variable
Replace that user's entry in `TINYAUTH_AUTH_USERS` with the new `username:hash:secret` string, then redeploy the stack.
::tip{icon=""}
✨ __Tip:__ Verify the flow works before relying on it:
```bash [Terminal]
sudo docker run -i -t --rm ghcr.io/tinyauthapp/tinyauth:v5 user verify --interactive
```
It re-prompts for the username, password, and current 6-digit code.
::
### Done !
::
From now on, that user needs both their password and a valid code from their authenticator app to log in.
## Exposing TinyAuth with Swag
TinyAuth needs its own subdomain: it's the page users land on before being forwarded to the app they actually want.
::note
We assume you have the subdomain `tinyauth.mydomain.com` with a `CNAME` pointing to `mydomain.com` in your [DNS zone](/general/networking/dns). And of course, [unless you use Cloudflare Zero Trust](/serveex/security/cloudflare), your box's port `443` must be forwarded to your server's port `443` in [NAT rules](/general/networking/nat).
::
::steps{level="3"}
### Add TinyAuth's network to SWAG
Go to Dockge and edit SWAG's compose file by adding TinyAuth's network:
```yaml [compose.yaml]
---
services:
swag:
container_name: # ...
# ...
networks: # Attach container to custom network
# ...
- tinyauth # Name of the declared network
networks: # Define the custom network
# ...
tinyauth: # Declared network name
name: tinyauth_default # Actual external network name
external: true # Marks it as externally defined
```
Redeploy the stack and wait for SWAG to be fully operational.
::note
Here we assume the TinyAuth network name is `tinyauth_default`. You can check the connection by visiting SWAG's dashboard at `http://yourserverip:81`.
::
### Create the subdomain.conf file
In the Swag folders, create the file `tinyauth.subdomain.conf`:
::tip{icon="" to="/serveex/files/file-browser-quantum"}
✨ __Tip:__ Use **File Browser Quantum** to navigate and edit files instead of using terminal commands.
::
```bash [Terminal]
sudo nano /srv/docker/swag/config/nginx/proxy-confs/tinyauth.subdomain.conf
```
Paste the following configuration:
```nginx [tinyauth.subdomain.conf]
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name tinyauth.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
location / {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app tinyauth;
set $upstream_port 3000;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
### Visit your new subdomain
Wait a few minutes, then open `https://tinyauth.mydomain.com` in your browser and log in with the username/password you created above.
::caution
__If it fails:__ check your firewall rules.
::
### Done !
::
## Protecting an app via reverse proxy
Swag doesn't ship a ready-made include file for TinyAuth, so we'll add the forward-auth check directly to the app's own `*.subdomain.conf`. We'll use Dockge as an example.
::steps{level="3"}
### Open the app's subdomain.conf file
```bash [Terminal]
sudo nano /srv/docker/swag/config/nginx/proxy-confs/dockge.subdomain.conf
```
### Add the forward-auth check
Add an internal `/tinyauth` location, and reference it from the app's `location /` block with `auth_request`:
```nginx [dockge.subdomain.conf]{9-11,25}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name dockge.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
location /tinyauth {
internal;
proxy_pass http://tinyauth:3000/api/auth/nginx;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
}
location @tinyauth_login {
return 302 https://tinyauth.mydomain.com/login?redirect_uri=$scheme://$http_host$request_uri;
}
location / {
auth_request /tinyauth;
error_page 401 = @tinyauth_login;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app dockge;
set $upstream_port 5001;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
::note{to="/serveex/security/tinyauth#exposing-tinyauth-with-swag"}
The `location /tinyauth` block runs inside SWAG's own container, so SWAG needs to be on TinyAuth's Docker network to reach it by name (`tinyauth` here). This should already be set up from **exposing TinyAuth itself**. If you run into an error, double-check SWAG's compose file still has that network attached.
::
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
### Done !
::
That's it! Visiting `https://dockge.mydomain.com` now redirects to TinyAuth first. Repeat this `location /tinyauth` / `auth_request` pattern in any other app's `*.subdomain.conf` to protect it the same way.
::note
Repeat this process for each app you want to protect (unless it has native OIDC support, in which case you can point it directly at Pocket ID instead).
::
## Leaving specific paths public
Sometimes you want most of an app locked behind TinyAuth, but a handful of paths left open, for example a public status page, or the API endpoints a mobile app relies on. Unlike Authentik, TinyAuth has no built-in "authenticated paths" setting for this: it's a plain nginx problem, and it's solved with nginx's own location matching.
A regex `location` block always takes priority over the plain `location /` block, no matter which one appears first in the file. So any path matched by a regex location you define runs its own `proxy_pass`, without ever reaching the `auth_request /tinyauth;` line in `location /`.
For example, to leave Uptime-Kuma's public status page and its assets open while protecting everything else:
```nginx [dockge.subdomain.conf]{9-16}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name stats.*;
include /config/nginx/ssl.conf;
location ~ ^/(status|assets|icon\.svg|api|upload|metrics) {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app uptime-kuma;
set $upstream_port 3001;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
location /tinyauth {
internal;
proxy_pass http://tinyauth:3000/api/auth/nginx;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
}
location @tinyauth_login {
return 302 https://tinyauth.mydomain.com/login?redirect_uri=$scheme://$http_host$request_uri;
}
location / {
auth_request /tinyauth;
error_page 401 = @tinyauth_login;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app uptime-kuma;
set $upstream_port 3001;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
::note
Adjust the list of excluded paths to what the app you're protecting actually needs public. Never leave an admin or settings path in that list, only what the app itself documents as safe to expose unauthenticated.
::
@@ -0,0 +1,282 @@
---
title: Pocket ID
description: Install Pocket ID, a lightweight self-hosted OIDC provider that lets you log in to your other apps with a passkey instead of a password.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
[Pocket ID](https://pocket-id.org) is a minimalist, self-hosted OIDC (OpenID Connect) provider built entirely around passkeys: instead of managing passwords, you and your users log in to compatible apps with a **passkey** (fingerprint, face unlock, or a hardware security key). It runs as a single lightweight container with no external database to manage, and it does exactly one thing well: issuing OIDC logins.
![pocketid](/img/serveex/pocketid.png)
This makes it a good fit if you just need a simple, fast SSO backend, for example to pair with [TinyAuth](/serveex/security/tinyauth) as a lightweight forward-auth setup, or to log in directly to apps that natively support OIDC.
- [Pocket ID documentation](https://pocket-id.org/docs)
- [Pocket ID on GitHub](https://github.com/pocket-id/pocket-id)
## Installation
::file-tree
---
tree:
/:
- srv:
- docker:
- pocket-id:
- compose.yaml
- .env
- data/
---
::
::steps{level="3"}
### Create the data folder
```bash [Terminal]
sudo mkdir -p /srv/docker/pocket-id/data
```
### Generate an encryption key
```bash [Terminal]
openssl rand -base64 32
```
Keep the output, you'll need it for the `.env` file below.
### Deploy the stack
Open Dockge, click `compose`, name the stack `pocket-id`, and add the following config:
```yaml [compose.yaml]
---
services:
pocket-id:
image: pocketid/pocket-id:v2
container_name: pocket-id
restart: unless-stopped
env_file:
- .env
volumes:
- /srv/docker/pocket-id/data:/app/data
ports:
- 1411:1411
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:1411/healthz"]
interval: 90s
timeout: 5s
retries: 3
```
::tip{icon=""}
✨ Add the Watchtower label to automate updates:
```yaml [compose.yaml]
---
services:
pocket-id:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
```
::
### Set your environment variables
Fill in the `.env` file:
```properties [.env]
APP_URL=https://id.mydomain.com
ENCRYPTION_KEY=
TRUST_PROXY=true
```
| Variable | Value | Example |
|----------|-------|---------|
| `APP_URL`{lang=properties} | The public URL you'll use to reach Pocket ID (see exposure below) | `https://id.mydomain.com` |
| `ENCRYPTION_KEY`{lang=properties} | The key generated above | `Q2pVEqsTNRkJSO9SkJzU3KZ2...` |
| `TRUST_PROXY`{lang=properties} | Required since Pocket ID sits behind Swag | `true` |
Deploy the stack. The local interface is available at `http://yourserverip:1411`.
### Done !
::
## First login
Pocket ID doesn't use passwords: your first account is created with a **passkey**, which your browser or OS will generate for you (Windows Hello, Touch ID, a phone, or a hardware key like a YubiKey).
- Go to `http://yourserverip:1411/setup`
- Follow the prompts to create your admin account and register your first passkey
::note
Since `APP_URL` is already set to your future public domain, passkey registration may ask you to open Pocket ID from that domain instead. Expose it first (see below) if setup doesn't complete locally.
::
## Exposing Pocket ID with Swag
Other apps need to reach Pocket ID over HTTPS to complete the OIDC login flow, so it must be exposed even if you only use it from home.
::note
We assume you have the subdomain `id.mydomain.com` with a `CNAME` pointing to `mydomain.com` in your [DNS zone](/general/networking/dns). And of course, [unless you use Cloudflare Zero Trust](/serveex/security/cloudflare), your box's port `443` must be forwarded to your server's port `443` in [NAT rules](/general/networking/nat).
::
::steps{level="3"}
### Add Pocket ID's network to SWAG
Go to Dockge and edit SWAG's compose file by adding Pocket ID's network:
```yaml [compose.yaml]
---
services:
swag:
container_name: # ...
# ...
networks: # Attach container to custom network
# ...
- pocket-id # Name of the declared network
networks: # Define the custom network
# ...
pocket-id: # Declared network name
name: pocket-id_default # Actual external network name
external: true # Marks it as externally defined
```
Redeploy the stack and wait for SWAG to be fully operational.
::note
Here we assume the Pocket ID network name is `pocket-id_default`. You can check the connection by visiting SWAG's dashboard at `http://yourserverip:81`.
::
### Create the subdomain.conf file
In the Swag folders, create the file `id.subdomain.conf`:
::tip{icon="" to="/serveex/files/file-browser-quantum"}
✨ __Tip:__ Use **File Browser Quantum** to navigate and edit files instead of using terminal commands.
::
```bash [Terminal]
sudo nano /srv/docker/swag/config/nginx/proxy-confs/id.subdomain.conf
```
Paste the following configuration:
```nginx [id.subdomain.conf]
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name id.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
location / {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app pocket-id;
set $upstream_port 1411;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
::caution
Don't put Pocket ID behind another authentication layer (TinyAuth, HTTP auth...). It's the identity provider itself, so locking it away would prevent anyone, including you, from logging in.
::
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
### Visit your new subdomain
Wait a few minutes, then open `https://id.mydomain.com` in your browser.
::caution
__If it fails:__ check your firewall rules.
::
### Done !
::
## Registering an OIDC client
To let another app (e.g. [TinyAuth](/serveex/security/tinyauth)) log in through Pocket ID, you need to register it as an OIDC client:
::steps{level="3"}
### Log in to Pocket ID
Go to `https://id.mydomain.com` and log in with your passkey.
### Create the OIDC client
Go to _Administration > OIDC Clients_, then click _Add OIDC Client_. Fill in a name (e.g. `TinyAuth`) and the app's callback URL (provided by the app you're protecting).
### Save your client credentials
Save, then copy the generated __Client ID__ and __Client Secret__. You'll need them in the other app's configuration.
### Done !
::
## Connecting Pocket ID to TinyAuth
[TinyAuth](/serveex/security/tinyauth) can delegate its login to Pocket ID instead of (or alongside) its local username/password, so anyone visiting a protected app authenticates with a passkey and gets forwarded through.
::steps{level="3"}
### Register TinyAuth as an OIDC client
[Register an OIDC client](#registering-an-oidc-client) named `TinyAuth`, using this callback URL:
```text
https://tinyauth.mydomain.com/api/oauth/callback/pocketid
```
### Add the Pocket ID provider in TinyAuth
Copy the __Client ID__ and __Client Secret__ Pocket ID gives you, then edit TinyAuth's `.env` file:
```bash [Terminal]
sudo nano /srv/docker/tinyauth/.env
```
Add the following:
```properties [.env]
TINYAUTH_OAUTH_PROVIDERS_POCKETID_NAME=Pocket ID
TINYAUTH_OAUTH_PROVIDERS_POCKETID_CLIENTID=
TINYAUTH_OAUTH_PROVIDERS_POCKETID_CLIENTSECRET=
TINYAUTH_OAUTH_PROVIDERS_POCKETID_AUTHURL=https://id.mydomain.com/authorize
TINYAUTH_OAUTH_PROVIDERS_POCKETID_TOKENURL=https://id.mydomain.com/api/oidc/token
TINYAUTH_OAUTH_PROVIDERS_POCKETID_USERINFOURL=https://id.mydomain.com/api/oidc/userinfo
TINYAUTH_OAUTH_PROVIDERS_POCKETID_REDIRECTURL=https://tinyauth.mydomain.com/api/oauth/callback/pocketid
TINYAUTH_OAUTH_PROVIDERS_POCKETID_SCOPES=openid email profile
```
| Variable | Value |
|----------|-------|
| `CLIENTID`{lang=properties} | The client ID copied from Pocket ID |
| `CLIENTSECRET`{lang=properties} | The client secret copied from Pocket ID |
| `AUTHURL` / `TOKENURL` / `USERINFOURL`{lang=properties} | Pocket ID's public URL, with the paths shown above |
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
### Redeploy the stack
Redeploy the TinyAuth stack. On your next visit to `https://tinyauth.mydomain.com`, you'll see a "Login with Pocket ID" option alongside the local login form.
::tip{icon=""}
✨ To skip straight to Pocket ID and hide the local login form, add `TINYAUTH_OAUTH_AUTOREDIRECT=pocketid` to the same `.env` file.
::
### Done !
::
That's it! TinyAuth now offers passwordless login via Pocket ID for every app it protects.
@@ -0,0 +1,2 @@
title: Monitoring
icon: i-lucide-chart-no-axes-column
@@ -0,0 +1,270 @@
---
title: Uptime-Kuma
description: Install Uptime-Kuma to monitor your self-hosted services uptime, set up alerts, and optionally protect the dashboard with Tinyauth or Authentik
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
![picture](https://user-images.githubusercontent.com/1336778/212262296-e6205815-ad62-488c-83ec-a5b0d0689f7c.jpg)
## Installation
::file-tree
---
tree:
/:
- srv:
- docker:
- uptime-kuma:
- data/
- compose.yaml
---
::
::steps{level="3"}
### Deploy the stack
Open Dockge, click on `compose`, name the stack `uptime-kuma`, then copy and paste the following:
```yaml [compose.yaml]
---
services:
uptime-kuma:
image: louislam/uptime-kuma:2-slim
container_name: uptime-kuma
volumes:
- /srv/docker/uptime-kuma/uptime-kuma-data:/app/data
ports:
- 3200:3001 # <Host Port>:<Container Port>
restart: always
```
::tip{icon=""}
✨ __Tip:__ Add the Watchtower label to each container to automate updates
```yaml [compose.yaml]
services:
uptime-kuma:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
```
::
### Access the web UI
You can now access the tool via `http://yourserverip:3200`.
::caution
__If it fails:__ check your firewall rules.
::
### Done !
::
## Expose with Swag
::note{icon=""}
📋 __Before you begin:__
<br/><br/>
We assume you have the subdomain `stats.mydomain.com` with a `CNAME` pointing to `mydomain.com` in your [DNS zone](/general/networking/dns). And of course, [unless you're using Cloudflare Zero Trust](/serveex/security/cloudflare), port `443` of your router should point to port `443` of your server via [NAT rules](/general/networking/nat).
::
::warning
Uptime-Kuma does not use multi-factor authentication. Exposing Uptime-Kuma on the internet could compromise the machines it monitors. Only do this if you're using an MFA system like [TinyAuth](/serveex/security/tinyauth) or [Authentik](/serveex/advanced/authentik/). Otherwise, dont expose it with SWAG; use a VPN like [Wireguard](/serveex/core/wireguard) instead.
::
::steps{level="3"}
### Create the subdomain.conf file
In the Swag folders, create the `stats.subdomain.conf` file.
::tip{icon="" to="/serveex/files/file-browser-quantum"}
✨ __Tip for those who dislike the terminal:__
you can use **File Browser Quantum** to browse and edit your files instead of using terminal commands.
::
```bash [Terminal]
sudo nano /srv/docker/swag/config/nginx/proxy-confs/stats.subdomain.conf
```
Paste the following config:
```nginx [stats.subdomain.conf]
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name stats.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth (requires ldap-location.conf in the location block)
#include /config/nginx/ldap-server.conf;
# enable for Authelia (requires authelia-location.conf in the location block)
#include /config/nginx/authelia-server.conf;
# enable for Authentik (requires authentik-location.conf in the location block)
#include /config/nginx/authentik-server.conf;
location / {
# enable the next two lines for http auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# enable for ldap auth (requires ldap-server.conf in the server block)
#include /config/nginx/ldap-location.conf;
# enable for Authelia (requires authelia-server.conf in the server block)
#include /config/nginx/authelia-location.conf;
# enable for Authentik (requires authentik-server.conf in the server block)
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app uptime-kuma;
set $upstream_port 3001;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
### Add Uptime-Kuma's network to SWAG
In Dockge, edit the SWAG compose and add the Uptime-Kuma network:
```yaml [compose.yaml]
---
services:
swag:
container_name: # ...
# ...
networks: # Link container to custom network
# ...
- uptime-kuma # Name of the declared network
networks: # Define custom network
# ...
uptime-kuma: # Name of the declared network
name: uptime-kuma_default # Actual name of the external network
external: true # Specifies it's an external network
```
Restart the stack and wait until SWAG is fully operational.
::note
Here we assume that the network name of Uptime-Kuma is `uptime-kuma_default`. You can verify the connection by visiting SWAG's dashboard at `http://yourserverip:81`.
::
### Done !
::
That's it! Uptime-Kuma is now exposed, and you can access it via `https://stats.mydomain.com`.
## Protecting Uptime-Kuma with TinyAuth
[TinyAuth](/serveex/security/tinyauth) can sit in front of Uptime-Kuma the same way as any other app, but here we also want the public status page (and the assets it needs to render) to stay reachable without logging in. This uses the same `location` regex technique as [Leaving specific paths public](/serveex/security/tinyauth#leaving-specific-paths-public), applied directly to `stats.subdomain.conf`.
::steps{level="3"}
### Open the subdomain.conf file
```bash [Terminal]
sudo nano /srv/docker/swag/config/nginx/proxy-confs/stats.subdomain.conf
```
### Add the forward-auth check and public paths
Replace the file's content with the following. The `location ~ ^/(...)` block matches Uptime-Kuma's public status page and its assets, and is served directly, without ever reaching the `auth_request` check in `location /`:
```nginx [stats.subdomain.conf]{9-16,32-33}
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name stats.*;
include /config/nginx/ssl.conf;
location ~ ^/(status|assets|icon\.svg|api|upload|metrics) {
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app uptime-kuma;
set $upstream_port 3001;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
location /tinyauth {
internal;
proxy_pass http://tinyauth:3000/api/auth/nginx;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
}
location @tinyauth_login {
return 302 https://tinyauth.mydomain.com/login?redirect_uri=$scheme://$http_host$request_uri;
}
location / {
auth_request /tinyauth;
error_page 401 = @tinyauth_login;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app uptime-kuma;
set $upstream_port 3001;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
::note{to="/serveex/security/tinyauth#exposing-tinyauth-with-swag"}
The `location /tinyauth` block runs inside SWAG's own container, so SWAG needs to be on TinyAuth's Docker network to reach it by name (`tinyauth` here). This should already be set up from **exposing TinyAuth itself**. If you run into an error, double-check SWAG's compose file still has that network attached.
::
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
### Done !
::
Redeploy the stack. Uptime-Kuma will then be publicly reachable via `https://stats.mydomain.com`, with the status page open and everything else behind TinyAuth.
::tip{icon=""}
✨ __Tip:__ You can also protect this app with [Authentik](/serveex/advanced/authentik) instead: open `stats.subdomain.conf` and uncomment the lines `include /config/nginx/authentik-server.conf;` and `include /config/nginx/authentik-location.conf;`. Dont forget to [create an application and provider in Authentik](/serveex/advanced/authentik#protecting-an-app-via-reverse-proxy). Then edit the Uptime-Kuma provider, and under *Advanced Protocol Settings > Authenticated Paths*, enter:
```properties
^/$
^/status
^/assets/
^/assets
^/icon.svg
^/api/.*
^/upload/.*
^/metrics
```
::
::tip{icon=""}
__Tip:__ If you're using [TinyAuth](/serveex/security/tinyauth) or [Authentik](/serveex/advanced/authentik) and don't mind exposing the admin panel to your local network, you can disable Uptime-Kuma's native authentication in its settings and rely solely on whichever one is protecting it.
::
@@ -0,0 +1,269 @@
---
title: Dozzle
description: Install Dozzle to monitor Docker container logs in real time from a clean web interface, exposed via SWAG.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
[Dozzle](https://dozzle.dev/) is a container that lets you access logs from your other containers and display them in real time through a user-friendly interface. It's a simple way to browse logs and retrieve information from the history.
![Dozzle](https://blog.unixhost.pro/wp-content/uploads/2023/03/image-5.png)
## Installation
::file-tree
---
tree:
/:
- srv:
- docker:
- dozzle:
- compose.yaml
- .env
- data/
---
::
::steps{level="3"}
### Deploy the stack
Open Dockge, click on `compose`, name the stack `dozzle`, then copy and paste the following:
```yaml [compose.yaml]
---
services:
dozzle:
container_name: dozzle
image: amir20/dozzle:latest
ports:
- 9135:8080
env_file:
- .env
environment:
- DOZZLE_HOSTNAME=${DOMAIN}
volumes:
- /var/run/docker.sock:/var/run/docker.sock
```
::tip{icon=""}
✨ __Tip:__ Add the watchtower label to each container to automate updates
```yaml [compose.yaml]
services:
dozzle:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
```
::
### Set your environment variables
Fill in your domain name in the `.env` file, for example:
```properties [.env]
DOMAIN=dozzle.mydomain.com
```
Deploy the container. Go to `http://yourserverip:9135`. Voilà, your Dozzle web UI is up and running!
### Done !
::
## Exposing Dozzle with Swag
::warning
Dozzle does not use multi-factor authentication. Exposing Dozzle to the internet could compromise the connected machines. Only do this if you use a multi-factor authentication system like [TinyAuth](/serveex/security/tinyauth) or [Authentik](/serveex/advanced/authentik/). Otherwise, do not expose it with SWAG and instead use a VPN like [Wireguard](/serveex/core/wireguard).
::
You may want to access Dozzle remotely and on all your devices. To do so, well expose Dozzle via Swag.
::note{icon=""}
📋 __Before you begin:__
<br/><br/>
We assume you have created a subdomain like `dozzle.mydomain.com` in your [DNS zone](/general/networking/dns) with a `CNAME` pointing to `mydomain.com` and that, [unless you're using Cloudflare Zero Trust](/serveex/security/cloudflare), youve redirected port `443` from your router to port `443` on your server in your [NAT rules](/general/networking/nat).
::
::steps{level="3"}
### Add Dozzle's network to SWAG
Go to Dockge and edit the SWAG compose file to add Dozzles network:
```yaml [compose.yaml]
---
services:
swag:
container_name: # ...
# ...
networks: # Connects the container to a custom network
# ...
- dozzle # Network name declared in the stack
networks: # Defines the custom network
# ...
dozzle: # Network name declared in the stack
name: dozzle_default # Actual name of the external network
external: true # Indicates it's an externally defined network
```
Redeploy the stack by clicking “Deploy” and wait for SWAG to be fully operational.
::note
We assume the Dozzle network name is `dozzle_default`. You can verify the connection is working by visiting the SWAG dashboard at `http://yourserverip:81`.
::
### Create the subdomain.conf file
In the Swag folder, create the `dozzle.subdomain.conf` file.
::tip{icon="" to="/serveex/files/file-browser-quantum"}
✨ __Tip:__ You can use **File Browser Quantum** to browse and edit files instead of using terminal commands.
::
```bash [Terminal]
sudo nano /srv/docker/swag/config/nginx/proxy-confs/dozzle.subdomain.conf
```
Paste the configuration below:
```nginx [dozzle.subdomain.conf]
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name dozzle.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth (requires ldap-location.conf in the location block)
#include /config/nginx/ldap-server.conf;
# enable for Authelia (requires authelia-location.conf in the location block)
#include /config/nginx/authelia-server.conf;
# enable for Authentik (requires authentik-location.conf in the location block)
#include /config/nginx/authentik-server.conf;
location / {
# enable the next two lines for http auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# enable for ldap auth (requires ldap-server.conf in the server block)
#include /config/nginx/ldap-location.conf;
# enable for Authelia (requires authelia-server.conf in the server block)
#include /config/nginx/authelia-location.conf;
# enable for Authentik (requires authentik-server.conf in the server block)
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app dozzle;
set $upstream_port 8080;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
### Done !
::
And there you go, Dozzle is now exposed!
## Protecting Dozzle with TinyAuth
Add [TinyAuth](/serveex/security/tinyauth)'s forward-auth check directly to `dozzle.subdomain.conf`, the same way as [the TinyAuth guide](/serveex/security/tinyauth#protecting-an-app-via-reverse-proxy):
```nginx [dozzle.subdomain.conf]{26-38,41-42}
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name dozzle.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth (requires ldap-location.conf in the location block)
#include /config/nginx/ldap-server.conf;
# enable for Authelia (requires authelia-location.conf in the location block)
#include /config/nginx/authelia-server.conf;
# enable for Authentik (requires authentik-location.conf in the location block)
#include /config/nginx/authentik-server.conf;
location /tinyauth {
internal;
proxy_pass http://tinyauth:3000/api/auth/nginx;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
}
location @tinyauth_login {
return 302 https://tinyauth.mydomain.com/login?redirect_uri=$scheme://$http_host$request_uri;
}
location / {
auth_request /tinyauth;
error_page 401 = @tinyauth_login;
# enable the next two lines for http auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# enable for ldap auth (requires ldap-server.conf in the server block)
#include /config/nginx/ldap-location.conf;
# enable for Authelia (requires authelia-server.conf in the server block)
#include /config/nginx/authelia-location.conf;
# enable for Authentik (requires authentik-server.conf in the server block)
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app dozzle;
set $upstream_port 8080;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
::note{to="/serveex/security/tinyauth#exposing-tinyauth-with-swag"}
The `location /tinyauth` block runs inside SWAG's own container, so SWAG needs to be on TinyAuth's Docker network to reach it by name (`tinyauth` here). This should already be set up from **exposing TinyAuth itself**. If you run into an error, double-check SWAG's compose file still has that network attached.
::
::tip{icon=""}
✨ __Tip:__ You can protect this app with [Authentik](/serveex/advanced/authentik) instead of TinyAuth by opening `dozzle.subdomain.conf` and removing the `#` in front of `include /config/nginx/authentik-server.conf;`{lang=nginx} and `include /config/nginx/authentik-location.conf;`{lang=nginx}. Dont forget to [create an application and a provider in Authentik](/serveex/advanced/authentik#protecting-an-app-via-reverse-proxy).
::
@@ -0,0 +1,274 @@
---
title: Speedtest Tracker
description: Install Speedtest Tracker to automatically measure and log your internet connection speed over time, exposed with SWAG.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
[Speedtest Tracker](https://docs.speedtest-tracker.dev/) is a container that allows you to schedule regular speed tests in order to log your server's internet connection status.
![speedtest-tracker](/img/serveex/speedtest-tracker.avif)
## Installation
::note{to="https://docs.linuxserver.io/images/docker-speedtest-tracker/"}
We will use the Docker image maintained by **LinuxServer.io**
::
::file-tree
---
tree:
/:
- srv:
- docker:
- speedtest-tracker:
- compose.yaml
- .env
- data:
- config/
---
::
::steps{level="3"}
### Generate an app key
In a terminal, generate a key using the following command:
```bash [Terminal]
echo -n 'base64:'; openssl rand -base64 32;
```
Take note of the key.
### Deploy the stack
Open Dockge, click on `compose`, name the stack `speedtest-tracker`, then paste the following:
```yaml [compose.yaml]
---
services:
speedtest-tracker:
image: lscr.io/linuxserver/speedtest-tracker:latest
restart: unless-stopped
container_name: speedtest-tracker
ports:
- ${PORT}:80
environment:
- PUID=${PUID}
- PGID=${GUID}
- TZ=Europe/Paris
- APP_KEY=${API_KEY}
- DB_CONNECTION=sqlite
- SPEEDTEST_SCHEDULE=${SCHEDULE}
volumes:
- /srv/docker/speedtest-tracker/data/config:/config
```
### Set your environment variables
Find your `PUID` and `GUID` by running the following command:
```bash [Terminal]
id yourusername
```
In the `.env` file, set the variable `API_KEY` with the key you generated and add a cron-style test schedule, as well as your `PUID` and `GUID`, for example:
```properties [.env]
SCHEDULE=15 */6 * * * # every 6 hours
API_KEY=base64:zihejehkj8_nzhY/OjeieR= # your key
PUID=1000
GUID=1000
PORT=3225 # port to access the web UI
```
::tip{icon="" to="https://docs.speedtest-tracker.dev/getting-started/environment-variables"}
✨ **Tip:** You can configure additional environment variables by referring to the **official documentation**.
::
Deploy the container and go to `http://yourserverip:3225`. Log in with the account `admin@exemple.com` and the password `password`. Dont forget to change your ID and password once logged in!
### Done !
::
## Exposing Speedtest Tracker with SWAG
::note
📋 **Prerequisites:**
We assume that you've already created a subdomain like `speedtest.yourdomain.com` in your [DNS zone](/general/networking/dns) with a `CNAME` pointing to `yourdomain.com`, and [unless youre using Cloudflare Zero Trust](/serveex/security/cloudflare), you've also forwarded port `443` from your router to port `443` of your server in your [NAT rules](/general/networking/nat).
::
Now we want to expose Speedtest Tracker to the internet so you can access it remotely. We assume you've set up the DNS `CNAME` for `speedtest.yourdomain.com` pointing to `yourdomain.com`.
::warning
Speedtest Tracker does not use multi-factor authentication. Exposing it on the internet could compromise connected devices. Do so only if you use a multi-factor system like [TinyAuth](/serveex/security/tinyauth) or [Authentik](/serveex/advanced/authentik/). Otherwise, avoid using SWAG and prefer a VPN like [Wireguard](/serveex/core/wireguard).
::
::steps{level="3"}
### Create the subdomain.conf file
Open the `speedtest.subdomain.conf` file:
```bash [Terminal]
sudo nano /srv/docker/swag/config/nginx/proxy-confs/speedtest.subdomain.conf
```
Configure it like this:
```nginx [speedtest.subdomain.conf]
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name speedtest.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# Authentication options (uncomment as needed)
#include /config/nginx/ldap-server.conf;
#include /config/nginx/authelia-server.conf;
#include /config/nginx/authentik-server.conf;
location / {
# Basic auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# Per-location authentication
#include /config/nginx/ldap-location.conf;
#include /config/nginx/authelia-location.conf;
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app speedtest-tracker;
set $upstream_port 3225;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Save and exit. The configuration will update in a few seconds.
### Add Speedtest Tracker's network to SWAG
::note
By default, SWAG doesnt know the name "speedtest-tracker". To allow access, you need to add Speedtest Trackers network to SWAGs `compose.yml`.
::
Go to Dockge, and edit SWAGs compose to include Speedtest Trackers network:
```yaml [compose.yaml]
---
services:
swag:
container_name: # ...
# ...
networks:
# ...
- speedtest-tracker
networks:
# ...
speedtest-tracker:
name: speedtest-tracker_default
external: true
```
Restart the stack by clicking "Deploy" and wait for SWAG to be fully up.
::note
This assumes the Speedtest Tracker network is named `speedtest-tracker_default`. You can verify the connection by visiting SWAGs dashboard at `http://yourserverip:81`.
::
### Done !
::
Wait a moment, then visit `https://speedtest.yourdomain.com` in your browser. You should be redirected to Speedtest Tracker. You can check service status via the dashboard (`http://yourserverip:81` from the local network).
## Protecting Speedtest Tracker with TinyAuth
Add [TinyAuth](/serveex/security/tinyauth)'s forward-auth check directly to `speedtest.subdomain.conf`, the same way as [the TinyAuth guide](/serveex/security/tinyauth#protecting-an-app-via-reverse-proxy):
```nginx [speedtest.subdomain.conf]{22-34,37-38}
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name speedtest.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# Authentication options (uncomment as needed)
#include /config/nginx/ldap-server.conf;
#include /config/nginx/authelia-server.conf;
#include /config/nginx/authentik-server.conf;
location /tinyauth {
internal;
proxy_pass http://tinyauth:3000/api/auth/nginx;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
}
location @tinyauth_login {
return 302 https://tinyauth.mydomain.com/login?redirect_uri=$scheme://$http_host$request_uri;
}
location / {
auth_request /tinyauth;
error_page 401 = @tinyauth_login;
# Basic auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# Per-location authentication
#include /config/nginx/ldap-location.conf;
#include /config/nginx/authelia-location.conf;
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app speedtest-tracker;
set $upstream_port 3225;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
::note{to="/serveex/security/tinyauth#exposing-tinyauth-with-swag"}
The `location /tinyauth` block runs inside SWAG's own container, so SWAG needs to be on TinyAuth's Docker network to reach it by name (`tinyauth` here). This should already be set up from **exposing TinyAuth itself**. If you run into an error, double-check SWAG's compose file still has that network attached.
::
::tip{icon=""}
✨ You can protect this app with [Authentik](/serveex/advanced/authentik) instead of TinyAuth by opening `speedtest.subdomain.conf` and uncommenting
`include /config/nginx/authentik-server.conf;` and `include /config/nginx/authentik-location.conf;`.
Dont forget to [create an application and provider in Authentik](/serveex/advanced/authentik#protecting-an-app-via-reverse-proxy).
::
@@ -0,0 +1,326 @@
---
title: Beszel
description: Install Beszel to monitor server CPU, RAM, disk, and network metrics, including remote servers, with a lightweight web dashboard.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
[Beszel](https://beszel.dev/) is a container that gives you real-time access to hardware information from your servers and allows historical tracking. CPU activity, disk usage, temperatures, RAM: nothing escapes your monitoring. Beszel also lets you configure notifications and alerts when your predefined thresholds are exceeded.
Beszel includes a hub with a web UI and an agent that collects data from your server or a remote server.
![Beszel](/img/serveex/beszel.png)
## Installation
::file-tree
---
tree:
/:
- srv:
- docker:
- beszel:
- compose.yaml
- .env
- data/
- socket/
---
::
::steps{level="3"}
### Deploy the stack
Open Dockge, click `compose`, name the stack `beszel`, and paste the following:
```yaml [compose.yaml]
---
services:
beszel:
image: henrygd/beszel:latest
container_name: beszel
restart: unless-stopped
ports:
- ${PORT}:8090
volumes:
- ./data:/beszel_data
- ./socket:/beszel_socket
beszel-agent:
image: henrygd/beszel-agent:latest
container_name: beszel-agent
restart: unless-stopped
network_mode: host
volumes:
- ./socket:/beszel_socket
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
LISTEN: /beszel_socket/beszel.sock
# Do not remove quotes around the key
KEY: ${KEY}
```
::tip{icon=""}
✨ __Tip:__ Add the Watchtower label to each container to automate updates.
```yaml [compose.yaml]
---
services:
beszel:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
```
::
### Set your environment variables
Fill out the `.env` file, for example:
```properties [.env]
PORT=8090 # web UI port
KEY= # private key to retrieve from Beszel when adding a system
```
For the `KEY` value, you'll need to launch Beszel once to get it.
Deploy the container and go to `http://yourserverip:8090`. Your Beszel web UI is now accessible!
### Done !
::
::caution
__If it fails:__ check your firewall rules.
::
### Add local server information
Now that the web UI is accessible, you need to push local server information into it. Just add a machine via the web UI and configure it like this:
![Beszel add system](/img/serveex/beszel-add.png)
Note the private key and confirm. Enter the key in your `.env` file in Dockge and redeploy the stack. Once done, your server will appear in the web UI:
![Beszel system](/img/serveex/beszel-system.png)
### Add a remote server
You can also monitor a remote server. To do so, run the agent on the remote server. Add a new machine in Beszel and fill in:
- The name displayed for your remote server
- The IP address or domain name of the remote server
- The listening port (e.g., `45876`)
Beszel will suggest a `compose.yaml` to deploy on the remote server, or you can use:
```yaml [compose.yaml]
---
services:
beszel-agent:
image: henrygd/beszel-agent
container_name: beszel-agent
restart: unless-stopped
network_mode: host
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
LISTEN: ${PORT}
KEY: ${KEY}
```
And in `.env`:
```properties [.env]
PORT=45876 # communication port between hub and remote agent
KEY= # private key from Beszel when adding the system
```
Deploy the stack on the remote server. Data will begin flowing into the web UI after a few seconds.
::caution
__If it fails:__ check your firewall rules.
::
## Expose Beszel with Swag
::warning
Beszel does not support multi-factor authentication. Exposing it on the internet could compromise connected machines. Only do this if you're using a system like [TinyAuth](/serveex/security/tinyauth) or [Authentik](/serveex/advanced/authentik/). Otherwise, do not expose with SWAG. Use a VPN like [Wireguard](/serveex/core/wireguard) instead.
::
If you want to access Beszel remotely from all your devices, expose it using Swag.
::note{icon="" to="/general/networking/nat"}
📋 __Prerequisite:__
<br/><br/>
You must have created a DNS subdomain like `beszel.mydomain.com` with a `CNAME` pointing to `mydomain.com`. Unless you're using Cloudflare Zero Trust, you must also have forwarded port `443` on your router to your servers `443` port via **NAT rules**.
::
::steps{level="3"}
### Add Beszel's network to SWAG
In Dockge, edit Swag's compose file and add Beszels network:
```yaml [compose.yaml]
---
services:
swag:
container_name: # ...
# ...
networks:
# ...
- beszel # network declared in the stack
networks:
# ...
beszel:
name: beszel_default # actual external network name
external: true
```
Redeploy the stack and wait for Swag to become fully operational.
::note
We assume the network name is `beszel_default`. You can check connectivity by visiting Swag's dashboard at `http://yourserverip:81`.
::
### Create the subdomain.conf file
In Swags config folders, create `beszel.subdomain.conf`.
::tip{icon="" to="/serveex/files/file-browser-quantum"}
✨ __Tip:__ Use **File Browser Quantum** to browse and edit files instead of terminal commands.
::
```bash [Terminal]
sudo nano /srv/docker/swag/config/nginx/proxy-confs/beszel.subdomain.conf
```
Paste:
```nginx [beszel.subdomain.conf]
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name beszel.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth
#include /config/nginx/ldap-server.conf;
# enable for Authelia
#include /config/nginx/authelia-server.conf;
# enable for Authentik
#include /config/nginx/authentik-server.conf;
location / {
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
#include /config/nginx/ldap-location.conf;
#include /config/nginx/authelia-location.conf;
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app beszel;
set $upstream_port 8090;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
### Done !
::
Thats it! Beszel is now exposed!
## Protecting Beszel with TinyAuth
Add [TinyAuth](/serveex/security/tinyauth)'s forward-auth check directly to `beszel.subdomain.conf`, the same way as [the TinyAuth guide](/serveex/security/tinyauth#protecting-an-app-via-reverse-proxy):
```nginx [beszel.subdomain.conf]{26-38,41-42}
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name beszel.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth
#include /config/nginx/ldap-server.conf;
# enable for Authelia
#include /config/nginx/authelia-server.conf;
# enable for Authentik
#include /config/nginx/authentik-server.conf;
location /tinyauth {
internal;
proxy_pass http://tinyauth:3000/api/auth/nginx;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
}
location @tinyauth_login {
return 302 https://tinyauth.mydomain.com/login?redirect_uri=$scheme://$http_host$request_uri;
}
location / {
auth_request /tinyauth;
error_page 401 = @tinyauth_login;
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
#include /config/nginx/ldap-location.conf;
#include /config/nginx/authelia-location.conf;
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app beszel;
set $upstream_port 8090;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
::note{to="/serveex/security/tinyauth#exposing-tinyauth-with-swag"}
The `location /tinyauth` block runs inside SWAG's own container, so SWAG needs to be on TinyAuth's Docker network to reach it by name (`tinyauth` here). This should already be set up from **exposing TinyAuth itself**. If you run into an error, double-check SWAG's compose file still has that network attached.
::
::tip{icon=""}
✨ You can protect this app with [Authentik](/serveex/advanced/authentik) instead of TinyAuth by opening `beszel.subdomain.conf` and removing the `#` in front of `include /config/nginx/authentik-server.conf;` and `include /config/nginx/authentik-location.conf;`. Dont forget to [create an application and provider in Authentik](/serveex/advanced/authentik#protecting-an-app-via-reverse-proxy).
::
@@ -0,0 +1,281 @@
---
title: UpSnap
description: Install UpSnap to remotely wake up machines on your local network via Wake-on-LAN, exposed with SWAG.
---
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
[UpSnap](https://github.com/seriousm4x/UpSnap) is a container that allows you to remotely power on, shut down, or put your machines to sleep. It mainly uses Wake-On-Lan (WoL) over the network and offers advanced features.
![Beszel](/img/serveex/upsnap.webp)
## Installation
::file-tree
---
tree:
/:
- srv:
- docker:
- upsnap:
- compose.yaml
- .env
- data/
---
::
::steps{level="3"}
### Deploy the stack
Open Dockge, click on `compose`, name the stack `upsnap`, then copy and paste the following:
```yaml [compose.yaml]
---
services:
upsnap:
container_name: upsnap
image: ghcr.io/seriousm4x/upsnap:5
network_mode: host
restart: unless-stopped
volumes:
- /srv/docker/upsnap/data:/app/pb_data
environment:
- TZ=Europe/Paris
- UPSNAP_SCAN_RANGE=${SCAN_RANGE}
- UPSNAP_SCAN_TIMEOUT=500ms
- UPSNAP_PING_PRIVILEGED=true
dns:
- ${DNS}
entrypoint: /bin/sh -c "./upsnap serve --http 0.0.0.0:8095"
healthcheck:
test: curl -fs "http://localhost:8095/api/health" || exit 1
interval: 10s
```
::tip{icon=""}
✨ __Tip:__ Add the watchtower label to each container to automate updates
```yaml [compose.yaml]
services:
upsnap:
#...
labels:
- com.centurylinklabs.watchtower.enable=true
```
::
### Set your environment variables
Fill in the `.env`, for example:
```properties [.env]
RANGE=192.168.1.0/24 # scans all devices on the local network with an IP between 192.168.0.1 and 192.168.1.255
DNS=192.168.1.1 # DNS IP to resolve domain names, typically your routers IP
```
Deploy the container and go to `http://yourserverip:8095`. Just follow the steps to create your account!
### Done !
::
::caution
__If it fails:__ check your firewall rules.
::
## Exposing UpSnap with Swag
::warning
UpSnap does not support multi-factor authentication. Exposing it on the internet could compromise connected machines. Do this only if you're using a multi-factor authentication system like [TinyAuth](/serveex/security/tinyauth) or [Authentik](/serveex/advanced/authentik/). Otherwise, avoid exposing it with SWAG and use a VPN like [Wireguard](/serveex/core/wireguard) instead.
::
You may want to access it remotely from all your devices. To do so, we'll expose UpSnap via Swag.
::note{icon=""}
📋 __Beforehand:__
<br/><br/>
We assume you've created a subdomain in your [DNS zone](/general/networking/dns), such as `upsnap.yourdomain.com` with a `CNAME` to `yourdomain.com`. Also, unless you're using Cloudflare Zero Trust, you should have already forwarded port `443` from your router to port `443` on your server in your [NAT rules](/general/networking/nat).
::
::steps{level="3"}
### Add UpSnap's network to SWAG
Go to Dockge, and edit the SWAG compose by adding the UpSnap network:
```yaml [compose.yaml]
---
services:
swag:
container_name: # ...
# ...
networks: # Connects the container to the custom network
# ...
- upsnap # Network name declared in the stack
networks: # Defines the custom network
# ...
upsnap: # Network name declared in the stack
name: upsnap_default # Actual name of the external network
external: true # Indicates it's an external network
```
Restart the stack by clicking "deploy" and wait for SWAG to be fully operational.
::note
Here we assume the network name for upsnap is `upsnap_default`. You can check the connection in the SWAG dashboard at `http://yourserverip:81`.
::
### Create the subdomain.conf file
In the Swag folders, create the file `upsnap.subdomain.conf`.
::tip{icon="" to="/serveex/files/file-browser-quantum"}
✨ __Tip:__ You can use **File Browser Quantum** to navigate your files and edit documents instead of using terminal commands.
::
```bash [Terminal]
sudo nano /srv/docker/swag/config/nginx/proxy-confs/upsnap.subdomain.conf
```
And paste the following configuration:
```nginx [upsnap.subdomain.conf]
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name upsnap.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth (requires ldap-location.conf in the location block)
#include /config/nginx/ldap-server.conf;
# enable for Authelia (requires authelia-location.conf in the location block)
#include /config/nginx/authelia-server.conf;
# enable for Authentik (requires authentik-location.conf in the location block)
#include /config/nginx/authentik-server.conf;
location / {
# enable the next two lines for http auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# enable for ldap auth (requires ldap-server.conf in the server block)
#include /config/nginx/ldap-location.conf;
# enable for Authelia (requires authelia-server.conf in the server block)
#include /config/nginx/authelia-location.conf;
# enable for Authentik (requires authentik-server.conf in the server block)
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app upsnap;
set $upstream_port 8095;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
### Done !
::
And thats it! Youve exposed UpSnap!
## Protecting UpSnap with TinyAuth
Add [TinyAuth](/serveex/security/tinyauth)'s forward-auth check directly to `upsnap.subdomain.conf`, the same way as [the TinyAuth guide](/serveex/security/tinyauth#protecting-an-app-via-reverse-proxy):
```nginx [upsnap.subdomain.conf]{26-38,41-42}
## Version 2023/12/19
server {
listen 443 ssl;
listen [::]:443 ssl;
server_name upsnap.*;
include /config/nginx/ssl.conf;
client_max_body_size 0;
#if ($lan-ip = yes) { set $geo-whitelist yes; }
#if ($geo-whitelist = no) { return 404; }
if ($geo-blacklist = no) { return 404; }
# enable for ldap auth (requires ldap-location.conf in the location block)
#include /config/nginx/ldap-server.conf;
# enable for Authelia (requires authelia-location.conf in the location block)
#include /config/nginx/authelia-server.conf;
# enable for Authentik (requires authentik-location.conf in the location block)
#include /config/nginx/authentik-server.conf;
location /tinyauth {
internal;
proxy_pass http://tinyauth:3000/api/auth/nginx;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Uri $request_uri;
}
location @tinyauth_login {
return 302 https://tinyauth.mydomain.com/login?redirect_uri=$scheme://$http_host$request_uri;
}
location / {
auth_request /tinyauth;
error_page 401 = @tinyauth_login;
# enable the next two lines for http auth
#auth_basic "Restricted";
#auth_basic_user_file /config/nginx/.htpasswd;
# enable for ldap auth (requires ldap-server.conf in the server block)
#include /config/nginx/ldap-location.conf;
# enable for Authelia (requires authelia-server.conf in the server block)
#include /config/nginx/authelia-location.conf;
# enable for Authentik (requires authentik-server.conf in the server block)
#include /config/nginx/authentik-location.conf;
include /config/nginx/proxy.conf;
include /config/nginx/resolver.conf;
set $upstream_app upsnap;
set $upstream_port 8095;
set $upstream_proto http;
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
}
}
```
::note{to="/serveex/security/tinyauth#exposing-tinyauth-with-swag"}
The `location /tinyauth` block runs inside SWAG's own container, so SWAG needs to be on TinyAuth's Docker network to reach it by name (`tinyauth` here). This should already be set up from **exposing TinyAuth itself**. If you run into an error, double-check SWAG's compose file still has that network attached.
::
::tip{icon=""}
✨ You can protect this app with [Authentik](/serveex/advanced/authentik) instead of TinyAuth by opening `upsnap.subdomain.conf` and removing the `#` in front of `include /config/nginx/authentik-server.conf;`{lang=nginx} and `include /config/nginx/authentik-location.conf;`{lang=nginx}. Dont forget to [create an application and provider in Authentik](/serveex/advanced/authentik#protecting-an-app-via-reverse-proxy).
::

Some files were not shown because too many files have changed in this diff Show More