Compare commits
68
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3529834d95 | ||
|
|
fd21bd2f83 | ||
|
|
2c28b27e82 | ||
|
|
d22711484b | ||
|
|
bfdef4c47a | ||
|
|
6129fd31d1 | ||
|
|
d839908521 | ||
|
|
c640801e23 | ||
|
|
9e3f116f55 | ||
|
|
a59c6c7d4a | ||
|
|
7f0c36fddf | ||
|
|
3e421f5478 | ||
|
|
ad86a1f3ce | ||
|
|
d2b6fbec5e | ||
|
|
a53a424399 | ||
|
|
77b87485b0 | ||
|
|
a63454ed53 | ||
|
|
104efa534c | ||
|
|
eaabf7db68 | ||
|
|
c1f6cff105 | ||
|
|
5d872ab3c6 | ||
|
|
8cfc044087 | ||
|
|
d08e86d04f | ||
|
|
4722c5c1a8 | ||
|
|
876d0a6ff5 | ||
|
|
6eaf8a5c94 | ||
|
|
45bca17862 | ||
|
|
66d51c4010 | ||
|
|
da67053e3b | ||
|
|
10040814e1 | ||
|
|
4ff886bab4 | ||
|
|
0b70a6f693 | ||
|
|
4af8bbe1e4 | ||
|
|
c89b16d0ae | ||
|
|
d2ae627ec6 | ||
|
|
dff7947b1b | ||
|
|
f72a4eefc9 | ||
|
|
a4d7ac0182 | ||
|
|
cf9c3a2b14 | ||
|
|
ebce70c352 | ||
|
|
f50ce5472c | ||
|
|
e82eaab851 | ||
|
|
211107e2ff | ||
|
|
d318c65d6c | ||
|
|
ad52d4a654 | ||
|
|
d72fb9f1ea | ||
|
|
71f8ce40d4 | ||
|
|
1d424bd197 | ||
|
|
c1d3d35e24 | ||
|
|
7059821f1c | ||
|
|
ba5047030a | ||
|
|
4fd1c8db1e | ||
|
|
208f95c5ab | ||
|
|
e30693d39e | ||
|
|
80465d7398 | ||
|
|
f5da0b4eb3 | ||
|
|
ccda4aa34f | ||
|
|
fd66282a60 | ||
|
|
6046093b24 | ||
|
|
7aadc4378c | ||
|
|
1654fd9287 | ||
|
|
a9284d1d85 | ||
|
|
5c207add10 | ||
|
|
e9ccda2e14 | ||
|
|
acdebcb682 | ||
|
|
675ecaee3a | ||
|
|
7630f970fe | ||
|
|
3e6bb5aa92 |
@@ -0,0 +1,14 @@
|
|||||||
|
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'
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
name: Trigger container build
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [main]
|
|
||||||
paths:
|
|
||||||
- 'content/**'
|
|
||||||
- 'app/**'
|
|
||||||
- 'public/**'
|
|
||||||
- 'server/**'
|
|
||||||
- 'nuxt.config.ts'
|
|
||||||
- 'content.config.ts'
|
|
||||||
- 'tsconfig.json'
|
|
||||||
- 'package.json'
|
|
||||||
- 'package-lock.json'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
trigger:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Dispatch docudjeex-build
|
|
||||||
run: |
|
|
||||||
curl -sf -X POST \
|
|
||||||
-H "Authorization: token ${{ secrets.BUILD_REPO_TOKEN }}" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-d "{\"ref\":\"main\",\"inputs\":{\"source_sha\":\"${{ github.sha }}\"}}" \
|
|
||||||
"https://git.djeex.fr/api/v1/repos/Djeex/docudjeex-build/actions/workflows/build.yml/dispatches"
|
|
||||||
Regular → Executable
+9
-42
@@ -1,45 +1,12 @@
|
|||||||
# Nuxt dev/build outputs
|
|
||||||
.output
|
|
||||||
.data
|
|
||||||
.nuxt
|
|
||||||
.nitro
|
|
||||||
.cache
|
|
||||||
dist
|
|
||||||
|
|
||||||
# Node dependencies
|
|
||||||
node_modules
|
node_modules
|
||||||
|
*.iml
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
|
|
||||||
# Misc
|
|
||||||
.DS_Store
|
|
||||||
.fleet
|
|
||||||
.idea
|
.idea
|
||||||
.eslintcache
|
*.log*
|
||||||
|
.nuxt
|
||||||
# Local env files
|
.vscode
|
||||||
|
.DS_Store
|
||||||
|
coverage
|
||||||
|
dist
|
||||||
|
sw.*
|
||||||
.env
|
.env
|
||||||
.env.*
|
.output
|
||||||
!.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
|
|
||||||
.screenshot
|
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
# 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` dir is always an absolute path.** An earlier revision branched this on `nuxi dev` vs a production build, passing the plain relative string `'public/'` in production on the theory that an absolute path there broke the `/_ipx` route's SVG `Content-Type`. In production that relative path doesn't reliably resolve to the project root at the moment the prerender crawler actually requests an `/_ipx/*` URL. The real-world result was every single image on the site 404ing (`IPX_FILE_NOT_FOUND`) in a from-scratch production build, not a content-type quirk on some of them. Always resolving the absolute path (`fileURLToPath(new URL('./public', import.meta.url))`) fixed it in both modes; since the whole site is prerendered to static files anyway, each image's correct extension is what actually decides its serving content-type, not this setting.
|
|
||||||
- **`icon.serverBundle.collections`.** Every icon collection referenced dynamically (built from a variable/string at runtime rather than written as a literal `i-xxx` somewhere) has to be listed here explicitly, or Nuxt Icon's static usage scanner never finds it and falls back to a live `api.iconify.design` request at prerender time, which times out (`[Icon] loading icon X timed out`) wherever outbound network access is restricted, exactly as it was on the actual deployment host. Two places build icon names this way: `app/app.config.ts`'s `ui.prose.codeIcon` map (`simple-icons`, `lucide`) and `app/components/content/FileTreeNode.vue`'s per-extension file-type icons (`vscode-icons`). Add any new collection here the moment a new dynamic icon source is introduced. Check it's actually installed first with `ls node_modules/@iconify-json/`.
|
|
||||||
- **`components:` array: keep `'~/components'` as the first entry.** Passing a `components` array to Nuxt *replaces* its default `~/components` auto-scan instead of adding to it. Every local override in `app/components/` (`AppHeaderCenter`, `DocsAsideLeftTop`, etc., all documented below) was silently shadowed by the docus layer's originals the moment a custom array was added without also re-listing the default scan. The override files were still there, just never picked up. The second entry, `{ path: '~/components/prose', pathPrefix: false, global: true }`, exists because `ProseNote`/`ProseTip`/`ProseWarning`/`ProseCaution` are *only* ever resolved dynamically by name from Nuxt Content's MDC tag map (`note` → `ProseNote`, etc.), and nothing statically writes `<ProseNote>` in a template, so Vite's production build tree-shook all four out of both the client and server bundles entirely, and every admonition on the live site rendered as a raw, unstyled `<ProseNote icon to="...">` tag instead of the actual callout. `nuxt dev` never surfaces either of these: it doesn't tree-shake, and it resolves components on demand regardless of the scan config.
|
|
||||||
- **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 (pre-rewrite, on the separate `docudjeex` repo's `main`/`french` branches) served English at the site root with no locale prefix (`/serveex/introduction/`) and French under `/fr/` with French-language slugs (`/fr/serveex/coeur/installation/`). This rewrite moved every URL under an explicit `/en/`/`/fr/` prefix with English-based slugs on both, which would otherwise break every external link, bookmark, and search-engine ranking built up on the old URLs. All 72 mappings use `statusCode: 301` explicitly, since 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. French pages whose old slug already matches the new one (`dozzle`, `immich`, and similar words that are spelled the same in both languages) are deliberately absent, since a rule there would redirect the page to itself. See the warning callout above this section: none of these 72 rules produce a real 301 on the actual production host, only a `location =` block in that host's nginx config does.
|
|
||||||
- **`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 `/robots.txt`. It exists as a server route yet was never written to the static output, so it 404s on a static host and loses the `Sitemap:` line that points crawlers at the sitemap. `/` is listed too, though it turns out this doesn't actually matter: Nitro's crawler always visits `/` on its own regardless (it's the crawl's entry point), confirmed by removing it from this array and finding it prerendered anyway. `@nuxtjs/i18n`'s `detectBrowserLanguage` never actually runs on this static host: `/` is baked as a static `<meta http-equiv="refresh">` stub whose target is a fixed string decided once at build time (`defaultLocale: 'en'`), not read per visitor, and Nitro bakes *any* route carrying a `redirect` routeRule the same way, with no real HTTP status. The real 301 for `/` lives in the production host's nginx config instead, see the warning callout above.
|
|
||||||
- **`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.
|
|
||||||
|
|
||||||
> **A `routeRules` redirect can't produce a real HTTP redirect on this deployment.** The production host serves this site as plain static files (nginx `root` + `try_files`, no `proxy_pass` to a running Nuxt/Nitro process at all). Any route with a `redirect` rule still gets crawled and prerendered like any other page, but since a static file can't carry a custom HTTP status, Nitro bakes it as a client-side `<meta http-equiv="refresh">` stub instead. That's invisible to anything that doesn't execute the page (search-engine link-equity, most link-preview tools, `curl`). This was tried for `/` (redirecting to `/en/`) and reverted after confirming the real 301 only appears when running `node .output/server/index.mjs` directly, never through the actual production nginx config. The 72 legacy-URL redirects in `routeRules` below have the exact same limitation: they were made real 301s by adding matching `location = /path { return 301 ...; }` blocks directly in the site's nginx config, outside this repo entirely, not by anything in `nuxt.config.ts`. If this project ever moves to a host that runs the Nitro server itself instead of serving `.output/public` as static files, all of this becomes unnecessary and `routeRules` redirects will work as real 301s on their own.
|
|
||||||
|
|
||||||
## `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.
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
# Functional changelog: site redesign (September 2026)
|
|
||||||
|
|
||||||
This document lists every change a visitor would actually notice between the old version of docu.djeex.fr and the new one. It does not cover technical implementation details, only what changed in the content and in using the site.
|
|
||||||
|
|
||||||
## New articles
|
|
||||||
|
|
||||||
- **"Linux tips for dummies" section** (under General): three new beginner-friendly articles on the Linux terminal.
|
|
||||||
- *CLI basics*: how a Linux command is built, with worked examples, plus a cheat-sheet table explaining where each common command's name comes from (`cd` for *change directory*, `ls` for *list*, and so on).
|
|
||||||
- *Filesystem*: Debian's folder and partition layout, and good practices around it.
|
|
||||||
- *Handy tools*: installing and using `btop`, `duf`, `ncdu`, `tldr`, `lazydocker`, and `ufw` (firewall).
|
|
||||||
- **Jellyfin**: new media server article, now the default recommendation in place of Plex (Plex is still documented, see below).
|
|
||||||
- **TinyAuth**: new article on this lightweight forward-auth proxy, for putting a login page in front of an app.
|
|
||||||
- **Pocket ID**: new article on this self-hosted OIDC provider, letting you log in with a passkey instead of a password.
|
|
||||||
- **File Browser Quantum**: new article on this modernized fork of File Browser, replacing the original as the recommended choice (see below).
|
|
||||||
- **Forgejo**: new article on this self-hosted Git platform, now the default recommendation in place of Gitea (Gitea is still documented, see below).
|
|
||||||
- **Arcane**: new article on this more advanced Docker management UI than Dockge, with support for multiple remote hosts and OIDC login.
|
|
||||||
- **"Confirm before deleting" tip**: new article documenting a Bash function that asks for confirmation before any `sudo`-run `rm`, to help avoid accidental deletions.
|
|
||||||
- **Section summary pages**: the General, Nonsense, and Recycled sections each now have a landing page listing their contents.
|
|
||||||
|
|
||||||
## Replaced or archived articles
|
|
||||||
|
|
||||||
Some apps are no longer the top recommendation, but their article stays available for anyone already using them or who prefers that alternative:
|
|
||||||
|
|
||||||
- **Plex** has been replaced by **Jellyfin** as the recommended choice. The Plex article is kept under a new "Alternatives" section, along with its related qBittorrent and Servarr variants.
|
|
||||||
- **Gitea** has been replaced by **Forgejo** as the recommended choice, following Gitea's governance change to a for-profit company. The Gitea article remains available under "Alternatives."
|
|
||||||
- **File Browser** has been replaced by **File Browser Quantum**. Unlike the two cases above, this one comes with an active warning rather than just being an alternative: the original project accumulated several serious security vulnerabilities (including one allowing full admin account takeover) and was officially abandoned by its maintainers in September 2026. The article is now marked deprecated with an explicit warning not to install it.
|
|
||||||
- **Overseerr** has been replaced by its successor **Seerr** in the automation (Servarr) article, following the merger of the Overseerr and Jellyseerr projects.
|
|
||||||
|
|
||||||
## Navigation reorganization
|
|
||||||
|
|
||||||
- **WireGuard** moved from the "Security" section to "Core": it's now treated as a basic infrastructure building block rather than an optional security tool.
|
|
||||||
- **Authentik** moved to a new **"Advanced"** section, alongside Arcane, for more complex setups aimed at users already comfortable with self-hosting.
|
|
||||||
- The "Security" section now only contains Cloudflare, TinyAuth, and Pocket ID.
|
|
||||||
- Deprecated or replaced articles (Plex, the old File Browser, Gitea, the old WireGuard setup) are now grouped under a new **"Recycled"** section, itself split into "Deprecated" (to avoid) and "Alternatives" (valid choices, just not the default recommendation).
|
|
||||||
|
|
||||||
## Content rewritten or substantially expanded
|
|
||||||
|
|
||||||
- **Debian installation**: heavily expanded article.
|
|
||||||
- Explains how to enable Wake-on-LAN and automatic restart after a power outage, directly in the BIOS.
|
|
||||||
- SSH connection instructions now split by operating system (macOS, Windows, Linux), with the exact commands for each.
|
|
||||||
- A note on temporarily re-enabling SSH password login when connecting a new machine to the server.
|
|
||||||
- A brand new section on waking the server up remotely (Wake-on-LAN from outside the local network), with a concrete port-forwarding example.
|
|
||||||
- A new section on keeping the system up to date, laid out step by step, plus a one-line command tip for fully automatic updates.
|
|
||||||
- A clear prerequisite added at the top: being comfortable with basic terminal commands, linking to the new "CLI basics" article.
|
|
||||||
- **Docker**: the introduction has been fully rewritten to explain what a container actually is and why this approach helps, instead of jumping straight into installation. The list of compatible apps is now presented as a set of examples (not exhaustive), each one linked.
|
|
||||||
- **Single sign-on**: several existing articles (Immich, Nextcloud, Forgejo, Pingvin, Vaultwarden) gained a new section explaining how to log in directly with Pocket ID, alongside the already-documented TinyAuth method.
|
|
||||||
- **TinyAuth protection**: added as a new section to many articles that didn't have it yet (monitoring, media, files, development), for putting these apps behind a login page.
|
|
||||||
|
|
||||||
## User experience (UX) changes
|
|
||||||
|
|
||||||
- **New look and feel**: the site was rebuilt from the ground up on a new component library, while keeping the old site's visual identity (colors, dark by default).
|
|
||||||
- **Navigation menu** repositioned to properly line up with the article content column (previously misaligned at some screen widths).
|
|
||||||
- **Collapsible sidebar**: the article tree on the left can now be collapsed, and starts collapsed by default instead of showing everything at once, easier to scan given how many sections there are.
|
|
||||||
- **Search button** added above the sidebar, visible on every page.
|
|
||||||
- **Step-by-step instructions**: nearly every install and configuration procedure is now visually numbered, instead of running paragraphs of text, much easier to follow.
|
|
||||||
- **Interactive folder trees**: folder structures shown in install guides are now real visual trees with folder/file icons, instead of ASCII-art text. Clicking a row copies that path to the clipboard.
|
|
||||||
- **Named, illustrated code blocks**: every code block now shows the real file name it belongs to, with a matching icon (for example, a YAML icon for a `compose.yaml`).
|
|
||||||
- **Callout boxes (notes, tips, warnings)**: colors matched to the old site's palette; some callouts can now be clicked entirely when they only contain a link to another article, instead of a plain inline text link.
|
|
||||||
- **Contributors shown at the bottom of every article**: lists everyone who has worked on a page (not just the last editor), linking to the full edit history.
|
|
||||||
- **"Other projects" links** (Gitea, Lumeex, Instameex) now also shown at the bottom of every article's table of contents, not just on the homepage.
|
|
||||||
- **Improved mobile layout**: homepage buttons, image, and spacing reorganized for small screens; "edit"/"report an issue" links now stack properly instead of overflowing on mobile.
|
|
||||||
|
|
||||||
## URL changes
|
|
||||||
|
|
||||||
- Every page now lives under an explicit language prefix, `/en/...` or `/fr/...`. Previously only French had a prefix (`/fr/...`), while English sat at the site root with no prefix at all.
|
|
||||||
- French URLs now use the same wording as their English counterparts (for example `/fr/general/networking/nat/` instead of the old `/fr/generalites/reseau/nat/`), so both languages share the exact same structure.
|
|
||||||
- **Every old link still works**: a bookmark, a link shared elsewhere, or a search-engine result pointing at an old address automatically redirects the visitor to its new equivalent.
|
|
||||||
|
|
||||||
## English/French parity
|
|
||||||
|
|
||||||
The French version is now a complete, faithful mirror of the English one: same articles, same organization, same formatting (same callouts, same numbered steps), with only the text translated. That wasn't the case before, where the two versions had drifted apart over time (articles present in one language but missing from the other, different organization).
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
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,10 +17,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
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.
|
|
||||||
@@ -1,69 +1,43 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="public/img/global/docudjeex-home.svg" align="center" width="700">
|
<img src="https://git.djeex.fr/Djeex/DjeexLab/raw/branch/main/docs/files/img/global/lab.svg" align="center" width="700">
|
||||||
|
|
||||||
[](https://docu.djeex.fr/)
|
[](https://docu.djeex.fr/)
|
||||||
[](https://docu.djeex.fr/)
|
[](https://docu.djeex.fr/)
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
# 🔧 Homelab docs & other dumb things
|
# 🔧 Docs, More Docs
|
||||||
|
|
||||||
**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/), on the [Docus](https://docus.dev) theme (Nuxt UI + Nuxt Content).
|
This documentation site is built using [Nuxt.js](https://nuxt.com/).
|
||||||
|
|
||||||
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.
|
This repository contains everything you need to edit pages, apply your changes, and redeploy the site.
|
||||||
|
|
||||||
## Requirements
|
## Setup
|
||||||
|
|
||||||
- Node.js 20 or later
|
|
||||||
- npm
|
|
||||||
|
|
||||||
## Getting started
|
|
||||||
|
|
||||||
Install dependencies:
|
Install dependencies:
|
||||||
|
|
||||||
```bash
|
```sh
|
||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
Start the dev server:
|
## Development Environment (port 3000)
|
||||||
|
|
||||||
```bash
|
```sh
|
||||||
npm run dev
|
npm run dev
|
||||||
```
|
```
|
||||||
|
|
||||||
The site will be available at `http://localhost:3000`.
|
## Generate Static Pages
|
||||||
|
|
||||||
## Build
|
```sh
|
||||||
|
npm run generate
|
||||||
```bash
|
|
||||||
npm run build
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This builds the production site (pointed at `https://docu.djeex.fr` via `NUXT_SITE_URL`) into `.output`. Run it with:
|
The HTML files will be generated in the `.output/public` folder and are ready to be deployed on any static-compatible hosting.
|
||||||
|
|
||||||
```bash
|
## Preview Build
|
||||||
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).
|
|
||||||
|
|||||||
@@ -0,0 +1,81 @@
|
|||||||
|
// 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: '',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
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
@@ -1,37 +0,0 @@
|
|||||||
/* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 8.8 KiB |
@@ -1,154 +0,0 @@
|
|||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 9.6 KiB |
@@ -1,84 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div />
|
|
||||||
</template>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
// Overrides Nuxt Content's default ProseImg (which renders every markdown
|
|
||||||
// image as a plain <img>, or through NuxtImg only if it happens to detect
|
|
||||||
// one registered): every raster image is re-encoded to WebP, which alone
|
|
||||||
// cuts most screenshots down substantially, and capped at 1280px wide.
|
|
||||||
// NuxtImg's `width` prop doubles as the rendered <img>'s HTML width
|
|
||||||
// attribute, not just the resize target, so passing 1280 unconditionally
|
|
||||||
// would stretch a smaller source (a 1024px screenshot, say) up to fill
|
|
||||||
// that width in the browser, blurry on every display. `imageWidths`
|
|
||||||
// (nuxt.config.ts's scanImageWidths) holds each image's real width, read
|
|
||||||
// once at build time, so undersized images are only re-encoded, not
|
|
||||||
// stretched. SVGs are already tiny vector files and would gain nothing
|
|
||||||
// from either step, so they're left untouched.
|
|
||||||
const props = defineProps<{
|
|
||||||
src?: string
|
|
||||||
alt?: string
|
|
||||||
width?: string | number
|
|
||||||
height?: string | number
|
|
||||||
}>()
|
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
|
||||||
const isSvg = computed(() => props.src?.toLowerCase().endsWith('.svg'))
|
|
||||||
const cappedWidth = computed(() => {
|
|
||||||
if (props.width) return props.width
|
|
||||||
const naturalWidth = props.src ? config.public.imageWidths[props.src] : undefined
|
|
||||||
return naturalWidth ? Math.min(naturalWidth, 1280) : 1280
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<img
|
|
||||||
v-if="isSvg"
|
|
||||||
:src="src"
|
|
||||||
:alt="alt"
|
|
||||||
:width="width"
|
|
||||||
:height="height"
|
|
||||||
>
|
|
||||||
<NuxtImg
|
|
||||||
v-else
|
|
||||||
:src="src"
|
|
||||||
:alt="alt"
|
|
||||||
:width="cappedWidth"
|
|
||||||
:height="height"
|
|
||||||
format="webp"
|
|
||||||
quality="100"
|
|
||||||
/>
|
|
||||||
</template>
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<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>
|
|
||||||
@@ -1,200 +0,0 @@
|
|||||||
<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 max-[420px]:flex-col"
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
@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;
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
@@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<img width="120" src="/img/logo.svg"/>
|
||||||
|
</template>
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
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 })
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|

|
||||||
|
#support
|
||||||
|
::card{icon=cib:gitea style="color:#1ad6ff;"}
|
||||||
|
#title
|
||||||
|
__git.djeex.fr__
|
||||||
|
#description
|
||||||
|
[Check my nonsense projects](https://git.djeex.fr)
|
||||||
|
::
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
::
|
||||||
|
::
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
icon: noto:star
|
||||||
|
navigation.title: About
|
||||||
|
navigation.redirect: /about/welcome
|
||||||
+21
-12
@@ -1,19 +1,27 @@
|
|||||||
---
|
---
|
||||||
|
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
|
||||||
|
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
::alert{type="info"}
|
||||||
|
🎯 __Goals:__
|
||||||
|
- Understand how port forwarding works
|
||||||
|
- Learn how to configure router NAT
|
||||||
|
- Learn how to issue DHCP leases (fixed IPs)
|
||||||
|
::
|
||||||
|
|
||||||
|

|
||||||

|
|
||||||
|
|
||||||
## 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.
|
||||||
|
|
||||||
@@ -24,7 +32,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 |
|
||||||
@@ -43,17 +51,18 @@ 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.
|
||||||
|
|
||||||
::warning{to="/serveex/core/swag"}
|
::alert{type="warning"}
|
||||||
|
:::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** to route traffic based on the request.
|
You must use a [Reverse Proxy](../../serveex/core/swag) 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.
|
||||||
|
|
||||||
+28
-17
@@ -1,59 +1,70 @@
|
|||||||
---
|
---
|
||||||
|
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"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 __Objectives:__
|
||||||
|
- 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.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 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`:
|
||||||
|
|
||||||
|
|
||||||
```
|
```
|
||||||
$TTL 3600
|
|
||||||
@ 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.
|
||||||
IN NS ns2.dns.me.
|
IN NS ns2.dns.me.
|
||||||
IN A 203.0.113.10
|
IN A 203.0.113.0
|
||||||
www IN CNAME mydomain.com.
|
www IN CNAME mydomain.com
|
||||||
subdomain 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).
|
||||||
- `IN SOA ns1.dns.me. dns.net. (...)` indicates `ns1.dns.me` as the primary DNS server, with refresh intervals.
|
- `IN SOA ns1.dns.me. dns.net. (...)` indicates `ns1.dns.me` as the primary DNS server, with refresh intervals.
|
||||||
- `IN NS` records define the authoritative name servers for the domain.
|
- `IN NS` records define the authoritative name servers for the domain.
|
||||||
- `IN A 203.0.113.10` means `mydomain.com` points to IP `203.0.113.10`.
|
- `IN A 203.0.113.0` means `mydomain.com` points to IP `203.0.113.0`.
|
||||||
- `subdomain IN CNAME mydomain.com.` means `subdomain.mydomain.com` points to the same destination as `mydomain.com`.
|
- `subdomain IN CNAME mydomain.com` means `subdomain.mydomain.com` points to the same destination as `mydomain.com`.
|
||||||
|
|
||||||
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.
|
||||||
|
|
||||||
::warning
|
::alert{type="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`.
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::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 won’t 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 won’t need to update the subdomain record.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
Most registrars offer user-friendly interfaces to manage DNS records. Refer to your registrar’s documentation for specific instructions.
|
Most registrars offer user-friendly interfaces to manage DNS records. Refer to your registrar’s documentation for specific instructions.
|
||||||
+72
-68
@@ -1,34 +1,43 @@
|
|||||||
---
|
---
|
||||||
|
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
|
||||||
|
|
||||||
|
Samba is a protocol that allows access to a folder located on a network drive. It can be configured on macOS, Windows, or Linux.
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
|
||||||
|
|
||||||
Samba is a free implementation of the SMB/CIFS protocol, allowing 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
|
||||||
|
::
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Create and configure a Shared Network Folder
|
|
||||||
::note
|
|
||||||
|
|
||||||
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`.
|
## Sharing a Network Folder
|
||||||
|
---
|
||||||
|
::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`.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Install Samba Server
|
### Install Samba Server
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
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
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo mkdir /video
|
sudo mkdir /video
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -36,19 +45,19 @@ sudo mkdir /video
|
|||||||
|
|
||||||
Now, edit the file `/etc/samba/smb.conf`.
|
Now, edit the file `/etc/samba/smb.conf`.
|
||||||
|
|
||||||
```bash [Terminal]
|
::alert{type="success"}
|
||||||
sudo nano /etc/samba/smb.conf
|
✨ __Tip:__ You can use [File Browser](/serveex/files/file-browser) to navigate and edit your files instead of using terminal commands.
|
||||||
```
|
|
||||||
|
|
||||||
::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`).
|
```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`).
|
||||||
|
|
||||||
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 [smb.conf]
|
```properties
|
||||||
[video]
|
[video]
|
||||||
comment = Video folder
|
comment = Video folder
|
||||||
path = /video
|
path = /video
|
||||||
@@ -60,75 +69,77 @@ Then scroll to the end of the file and add the following configuration:
|
|||||||
inherit permissions = yes
|
inherit permissions = yes
|
||||||
```
|
```
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Esc` to exit insert mode, then type `:x` and press `Enter` to save and 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.
|
||||||
|
|
||||||
::steps{level="3"}
|
Create the group:
|
||||||
### Create the group
|
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo groupadd smbshare
|
sudo groupadd smbshare
|
||||||
```
|
```
|
||||||
|
|
||||||
Give the group control over the `/video` folder:
|
Give the group control over the `/video` folder:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo chgrp -R smbshare /video
|
sudo chgrp -R smbshare /video
|
||||||
```
|
```
|
||||||
|
|
||||||
Set inherited permissions:
|
Set inherited permissions:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
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.
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
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:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo usermod -aG smbshare sambauser
|
sudo usermod -aG smbshare sambauser
|
||||||
```
|
```
|
||||||
|
|
||||||
Set a Samba password:
|
Set a Samba password:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo smbpasswd -a sambauser
|
sudo smbpasswd -a sambauser
|
||||||
```
|
```
|
||||||
|
|
||||||
### Enable the Samba account
|
Enable the Samba account:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
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
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
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`.
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo mkdir /mnt/video
|
sudo mkdir /mnt/video
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -138,39 +149,34 @@ 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:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo mkdir /smb
|
sudo mkdir /smb
|
||||||
sudo nano /smb/.credentials
|
sudo vi /smb/.credentials
|
||||||
```
|
```
|
||||||
|
|
||||||
Write:
|
Enter insert mode (`i`) and write:
|
||||||
|
|
||||||
```properties [.credentials]
|
```properties
|
||||||
username=sambauser
|
username=smbuser
|
||||||
password=password
|
password=password
|
||||||
```
|
```
|
||||||
|
|
||||||
* `sambauser`: 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 :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Esc`, then `:x` and `Enter` to save and exit.
|
||||||
|
|
||||||
Set proper file permissions:
|
Set proper file permissions:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
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:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
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
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -178,7 +184,7 @@ Replace `remote-ip` with your `remote-machine`'s IP address.
|
|||||||
|
|
||||||
Verify the mount:
|
Verify the mount:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo mount -t cifs
|
sudo mount -t cifs
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -186,40 +192,38 @@ You’ll 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`!
|
||||||
|
|
||||||
### (Optional) Auto-mount on Boot
|
### 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:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
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:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
echo "//remote-ip/video /mnt/video cifs _netdev,nofail,credentials=/smb/.credentials,x-systemd.automount,x-systemd.device-timeout=15 0 0" | sudo tee -a /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:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo reboot
|
sudo reboot
|
||||||
```
|
```
|
||||||
|
|
||||||
After rebooting, verify the mount:
|
After rebooting, verify the mount:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo mount -t cifs
|
sudo mount -t cifs
|
||||||
```
|
```
|
||||||
### And done!
|
|
||||||
::
|
|
||||||
|
|
||||||
::tip
|
And done!
|
||||||
__Unmount the Shared Folder__
|
|
||||||
|
|
||||||
```bash [Terminal]
|
### Unmount the Shared Folder
|
||||||
|
|
||||||
|
```sh
|
||||||
sudo umount -t cifs /mnt/video
|
sudo umount -t cifs /mnt/video
|
||||||
```
|
```
|
||||||
::
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
navigation.title: Networking
|
||||||
|
icon: lucide:network
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
---
|
---
|
||||||
|
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_
|
||||||
|
|
||||||
@@ -22,28 +24,29 @@ 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)
|
||||||
|
|
||||||
::warning
|
::alert{type="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="Disks with no RAID redundancy" 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;">
|
||||||
<ul>
|
<ul>
|
||||||
<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="RAID 0 striping data across disks" 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;">
|
||||||
<ul>
|
<ul>
|
||||||
<li>OS sees 1 drive.</li>
|
<li>OS sees 1 drive.</li>
|
||||||
<li>Data is striped across all disks.</li>
|
<li>Data is striped across all disks.</li>
|
||||||
@@ -51,15 +54,15 @@ 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="RAID 1 mirroring data across disks" 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;">
|
||||||
<ul>
|
<ul>
|
||||||
<li>OS sees 1 drive.</li>
|
<li>OS sees 1 drive.</li>
|
||||||
<li>All disks contain identical data.</li>
|
<li>All disks contain identical data.</li>
|
||||||
@@ -67,18 +70,18 @@ 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 disk’s 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 disk’s capacity, making it an expensive solution.
|
||||||
|
|
||||||
::tip{icon=""}
|
::alert{type="success"}
|
||||||
✨ __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="RAID 5 with distributed parity" style="max-width: 40%; margin-right: 20px;">
|
<img src="/img/global/raid5.svg" alt="Image" style="max-width: 40%; margin-right: 20px;">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
- OS sees 1 drive.
|
- OS sees 1 drive.
|
||||||
@@ -91,8 +94,9 @@ 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="RAID 6 with double distributed parity" style="max-width: 50%; margin-right: 20px;">
|
<img src="/img/global/raid6.svg" alt="Image" style="max-width: 50%; margin-right: 20px;">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
- OS sees 1 drive.
|
- OS sees 1 drive.
|
||||||
@@ -102,7 +106,7 @@ Use RAID 5 when you want reliable storage with 3 to 5 disks and minimal space lo
|
|||||||
- Total capacity is the sum of all disks minus two (e.g., 4×10TB = 20TB).
|
- Total capacity is the sum of all disks minus two (e.g., 4×10TB = 20TB).
|
||||||
- Minimum of 4 disks (6 recommended to minimize space loss).
|
- Minimum of 4 disks (6 recommended to minimize space loss).
|
||||||
|
|
||||||
Use RAID 6 in similar situations as RAID 5, especially with 6 or more disks. More disks mean higher failure risk. RAID 6 offers peace of mind by tolerating two simultaneous failures. There's nothing more frustrating than losing a second disk while the array is still rebuilding from replacing the first.
|
Use RAID 6 in similar situations as RAID 5, especially with 6 or more disks. More disks mean higher failure risk. RAID 6 offers peace of mind by tolerating two simultaneous failures.
|
||||||
|
|
||||||
## Software RAID
|
## Software RAID
|
||||||
(coming soon)
|
(coming soon)
|
||||||
@@ -1,22 +1,26 @@
|
|||||||
---
|
---
|
||||||
|
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"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 __Objectives:__
|
||||||
|
- 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.
|
||||||
Here’s what we’re most interested in:
|
Here’s what we’re 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).
|
||||||
@@ -24,7 +28,8 @@ Here’s what we’re most interested in:
|
|||||||
- Its robust error notifications and monitoring.
|
- Its robust error notifications and monitoring.
|
||||||
|
|
||||||
## Structure
|
## Structure
|
||||||

|
---
|
||||||
|

|
||||||
|
|
||||||
ZFS has a unique structure:
|
ZFS has a unique structure:
|
||||||
|
|
||||||
@@ -33,35 +38,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 disks, similar to traditional [RAID](/general/storage/raid) setups, but 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 natively: no external RAID software needed.
|
ZFS handles all this natively—no external RAID software needed.
|
||||||
|
|
||||||
::note{to="/general/storage/raid"}
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
Check out the **article on RAID** to find the right solution for your needs.
|
- Check out the [article on RAID](/general/storage/raid) to find the right solution for your needs.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
### Snapshots and Clones
|
### Snapshots and Clones
|
||||||
|
|
||||||
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.
|
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
|
||||||
|
|
||||||
@@ -69,4 +74,4 @@ ZFS can compress data on the fly (transparently to the user), saving disk space.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Now you know why ZFS is *the* file system to deploy on your NAS.
|
Now you know why ZFS is *the* file system to deploy on your NAS.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
navigation.title: Storage
|
||||||
|
icon: lucide:hard-drive
|
||||||
+50
-34
@@ -1,27 +1,34 @@
|
|||||||
---
|
---
|
||||||
|
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"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 __Objectives:__
|
||||||
|
- Understand the fundamentals of server hardware
|
||||||
|
::
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
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
|
||||||
@@ -30,28 +37,30 @@ 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="CPU icon" 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>
|
||||||
|
|
||||||
::warning
|
::alert{type="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="RAM icon" style="max-width: 65%;">
|
<img src="/img/global/ram.svg" alt="Image" style="max-width: 65%;">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
__RAM__ (Random Access Memory) is fast, temporary memory used by the CPU (and iGPU if applicable) for quick access during execution. It clears periodically and when the machine powers down. Better RAM = better CPU performance.
|
__RAM__ (Random Access Memory) is fast, temporary memory used by the CPU (and iGPU if applicable) for quick access during execution. It clears periodically and when the machine powers down. Better RAM = better CPU performance.
|
||||||
@@ -59,67 +68,72 @@ __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. [Jellyfin](/serveex/media/jellyfin)) 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. [Plex](/serveex/media/plex)) 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 iGPU, an integrated GPU built into the CPU (such as the N100/N150 or N305/N355), which uses the system’s 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 system’s shared RAM.
|
||||||
|
|
||||||
## HDD(s)
|
### HDD(s)
|
||||||
|
---
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="/img/global/hdd.svg" alt="Hard disk drive icon" 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 heads, somewhat 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 doesn’t 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 doesn’t 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.
|
||||||
|
|
||||||
::tip{icon="" to="/general/storage/raid"}
|
::alert{type="success"}
|
||||||
✨ __Tip:__ Use multiple HDDs in **RAID** to enhance performance and redundancy.
|
✨ __Tip:__ Use multiple HDDs in [RAID](/general/storage/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="NVMe SSD icon" style="max-width: 50%; margin-right: 20px;">
|
<img src="/img/global/nvme.svg" alt="Image" style="max-width: 50%; margin-right: 20px;">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
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 fast, offering 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 quickly, such as websites, applications, or processing workloads, should 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 ports, often Ethernet ports, can 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 (1.25 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).
|
||||||
- __RJ45 5G:__ Same connector, supporting up to 5 Gbps (625 MB/s).
|
- __RJ45 5G:__ Same connector, supporting up to 5 Gbps (625 MB/s).
|
||||||
- __RJ45 10G Base-T:__ Same RJ45 format, supporting up to 10 Gbps (1.25 GB/s).
|
- __RJ45 10G Base-T:__ Same RJ45 format, supporting up to 10 Gbps (1.25 GB/s).
|
||||||
- __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.
|
||||||
|
|
||||||
::warning
|
::alert{type="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.
|
||||||
|
|
||||||
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
|
||||||
@@ -127,7 +141,8 @@ __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 machine’s 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 machine’s 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.
|
||||||
|
|
||||||
@@ -141,10 +156,11 @@ 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 system’s 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 system’s 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="Computer case icon" 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;">
|
||||||
<p>The <strong>case</strong> is also an essential component of your machine. It plays a key role in cooling, through its fans and airflow design, and it determines the form factor compatibility for your motherboard, power supply, and any dedicated GPU you may install.
|
<p>The <strong>case</strong> is also an essential component of your machine. It plays a key role in cooling, through its fans and airflow design, and it determines the form factor compatibility for your motherboard, power supply, and any dedicated GPU you may install.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
+34
-22
@@ -1,58 +1,67 @@
|
|||||||
---
|
---
|
||||||
|
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"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 __Objectives:__
|
||||||
|
- Understand the basics of networking hardware
|
||||||
|
::
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
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 home: it'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, you’ll 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 router’s 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 you’re 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.
|
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, you’ll 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 router’s 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 you’re 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 speed: these 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.
|
||||||
|
|
||||||
::tip{icon=""}
|
::alert{type="success"}
|
||||||
✨ __Tip:__
|
✨ __Tip:__
|
||||||
You can easily use a third-party router to manage your network if your ISP’s 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 ISP’s 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.
|
||||||
|
|
||||||
::warning
|
::alert{type="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.
|
||||||
|
|
||||||
Here’s a quick reference of the most common cable and port standards:
|
Here’s a quick reference of the most common cable and port standards:
|
||||||
|
|
||||||
- **RJ45 Gigabit Ethernet 10/100/1000**: The standard RJ45 connector, supporting speeds from 10 Mbps (1.25 MB/s) 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) to 1000 Mbps (125 MB/s)
|
||||||
- **RJ45 2.5G**: Same connector, supporting speeds up to 2.5 Gbps (312.5 MB/s)
|
- **RJ45 2.5G**: Same connector, supporting speeds up to 2.5 Gbps (312.5 MB/s)
|
||||||
- **RJ45 5G**: Same connector, supporting speeds up to 5 Gbps (625 MB/s)
|
- **RJ45 5G**: Same connector, supporting speeds up to 5 Gbps (625 MB/s)
|
||||||
- **RJ45 10GBase-T**: Same connector, supporting speeds up to 10 Gbps (1.25 GB/s)
|
- **RJ45 10GBase-T**: Same connector, supporting speeds up to 10 Gbps (1.25 GB/s)
|
||||||
@@ -82,11 +91,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 ISP’s 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 ISP’s 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)
|
||||||
@@ -96,26 +105,29 @@ 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.
|
||||||
|
|
||||||
::warning
|
::alert{type="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
|
||||||
|
|
||||||
::warning
|
::alert{type="warning"}
|
||||||
|
:::list{type="warning"}
|
||||||
__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.
|
- 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.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
+18
-14
@@ -1,47 +1,50 @@
|
|||||||
---
|
---
|
||||||
|
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}
|
||||||
|
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
# The ProloNAS
|
||||||
|
|
||||||
::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?”_, _“Isn’t it expensive?”_. And often, people either give up or end up buying a proprietary NAS that they’ll 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?”_, _“Isn’t it expensive?”_. And often, people either give up or end up buying a proprietary NAS that they’ll throw away a year later once they realize it only brings headaches and wasted money.
|
||||||
|
|
||||||
A server isn’t a piece of furniture. It’s simply any computer capable of running Linux. That’s why mini PCs powered by **Intel N100** processors are so popular: for around $100–130 on Chinese platforms, you can get a machine that runs **24/7** for years, capable of handling everything you’d expect from a home server or personal cloud without sacrificing performance.
|
A server isn’t a piece of furniture. It’s simply any computer capable of running Linux.That’s why mini PCs powered by **Intel N100** processors are so popular: for around $100–130 on Chinese platforms, you can get a machine that runs **24/7** for years, capable of handling everything you’d expect from a home server or personal cloud without sacrificing performance.
|
||||||
|
|
||||||
It’s **objectively inexpensive**, and anyone with a bit of curiosity can get started.
|
It’s **objectively inexpensive**, and anyone with a bit of curiosity can get started.
|
||||||
|
|
||||||
A mini PC for $100 + a USB dock for $50 that holds multiple hard drives = a complete platform for $150, versus **$350–1200** for branded NAS systems.
|
A mini PC for $100 + a USB dock for $50 that holds multiple hard drives = a complete platform for $150, versus **$350–1200** for branded NAS systems.
|
||||||
|
|
||||||
That’s all a **ProloNAS** is: a NAS anyone can afford. The name is a French pun, *prolo* (slang for "working-class") plus *NAS*, a home server for the rest of us, not just people who can drop $1000 on a branded box. It’s then up to you to scale your storage capacity according to your needs.
|
That’s all a **ProloNAS** is. It’s then up to you to scale your storage capacity according to your needs.
|
||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
## 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 [Jellyfin](/serveex/media/jellyfin). 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 [Plex](/serveex/media/plex). 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?
|
||||||
|
|
||||||
Sure, a simple mini PC with 1–2 TB of storage will do for most people. And your movie collection might fit on an external drive of a few extra terabytes. But that’s **neither reliable nor scalable**: a single shock or hardware failure could permanently destroy your data.
|
Sure, a simple mini PC with 1–2 TB of storage will do for most people.And your movie collection might fit on an external drive of a few extra terabytes. But that’s **neither reliable nor scalable** a single shock or hardware failure could permanently destroy your data.
|
||||||
|
|
||||||
A real NAS is built around **storage reliability**. It uses redundancy strategies like [RAID](/general/storage/raid) to protect against drive failure, and snapshot systems like [ZFS](/general/storage/zfs) to guard against corruption.
|
A real NAS is built around **storage reliability**. It uses redundancy strategies like [RAID](/general/storage/raid) to protect against drive failure, and snapshot systems like [ZFS](/general/storage/zfs) to guard against corruption.
|
||||||
|
|
||||||
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.
|
||||||
@@ -77,3 +80,4 @@ In short, you have **no control** over a product that isn’t 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 @@
|
|||||||
|
navigation.title: Hardware
|
||||||
|
icon: lucide:server
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
icon: noto:open-book
|
||||||
|
navigation.title: General
|
||||||
|
navigation.redirect: /general/networking/nat
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
---
|
||||||
|
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
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**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
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
::
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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 you’ll 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
|
||||||
|
:::
|
||||||
|
::
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
---
|
||||||
|
navigation: true
|
||||||
|
title: Docker
|
||||||
|
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"}
|
||||||
|
🎯 __Goals:__
|
||||||
|
- Install [Docker](https://www.docker.com/)
|
||||||
|
- Install [Dockge](https://github.com/louislam/dockge) to manage stacks
|
||||||
|
- Install [Watchtower](https://github.com/containrrr/watchtower) to update containers
|
||||||
|
::
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Install Docker
|
||||||
|
---
|
||||||
|
Add the Docker repositories and GPG key:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Add Docker's official GPG key:
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install ca-certificates curl
|
||||||
|
sudo install -m 0755 -d /etc/apt/keyrings
|
||||||
|
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
|
||||||
|
sudo chmod a+r /etc/apt/keyrings/docker.asc
|
||||||
|
|
||||||
|
# Add the repository to Apt sources:
|
||||||
|
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
|
||||||
|
sudo apt-get update
|
||||||
|
```
|
||||||
|
|
||||||
|
Install the packages:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
That's it!
|
||||||
|
|
||||||
|
**More options:** [Install Docker for Debian 13](https://docs.docker.com/engine/install/debian/)
|
||||||
|
|
||||||
|
::alert{type="info" icon="exclamation-circle"}
|
||||||
|
:::list{type="info"}
|
||||||
|
- From here on, we assume the stacks are installed in the `/docker` folder, created using the command:
|
||||||
|
:::
|
||||||
|
```sh
|
||||||
|
sudo mkdir /docker
|
||||||
|
::
|
||||||
|
|
||||||
|
## 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 that’s lighter and easier for beginners than using the CLI or Portainer.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
File structure we will create:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
root
|
||||||
|
└── docker
|
||||||
|
└── dockge
|
||||||
|
└── compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Create the stack folder:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /docker
|
||||||
|
sudo mkdir dockge
|
||||||
|
```
|
||||||
|
|
||||||
|
Then create the `compose.yml` file in this folder using `vim`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /docker/dockge
|
||||||
|
sudo vi compose.yml
|
||||||
|
```
|
||||||
|
Press `i` to enter insert mode and paste the following:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
services:
|
||||||
|
dockge:
|
||||||
|
image: louislam/dockge:1
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: dockge
|
||||||
|
ports:
|
||||||
|
- 3555:5001 # LAN-accessible port will be 3555
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
- /docker/dockge/data:/app/data
|
||||||
|
- /docker:/docker
|
||||||
|
environment:
|
||||||
|
- DOCKGE_STACKS_DIR=/docker
|
||||||
|
```
|
||||||
|
|
||||||
|
Press `Esc` and type `:x` to save and exit.
|
||||||
|
|
||||||
|
To launch the container:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd /docker/dockge
|
||||||
|
sudo docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 is a container that checks for updates and pulls new images automatically, just by adding a label in your containers’ `compose.yml` files.
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
|
||||||
|
- Open Dockge in your browser
|
||||||
|
- Click `compose`
|
||||||
|
- Name the stack `watchtower`
|
||||||
|
- Paste the config below into the default config area in Dockge
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
services:
|
||||||
|
watchtower:
|
||||||
|
container_name: watchtower
|
||||||
|
image: ghcr.io/nicholas-fedor/watchtower:latest
|
||||||
|
restart: unless-stopped
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
- TZ=Europe/Paris
|
||||||
|
- WATCHTOWER_SCHEDULE=${SCHEDULE}
|
||||||
|
- WATCHTOWER_LABEL_ENABLE=true
|
||||||
|
- WATCHTOWER_CLEANUP=true
|
||||||
|
- WATCHTOWER_REMOVE_VOLUMES=true
|
||||||
|
# Discord notifications - uncomment if used
|
||||||
|
#- WATCHTOWER_NOTIFICATIONS=slack
|
||||||
|
#- WATCHTOWER_NOTIFICATION_SLACK_IDENTIFIER=Watchtower
|
||||||
|
#- WATCHTOWER_NOTIFICATION_SLACK_HOOK_URL=${WH_URL}
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
```
|
||||||
|
|
||||||
|
Then fill in the `.env` section in Dockge with the following:
|
||||||
|
|
||||||
|
```properties
|
||||||
|
SCHEDULE=
|
||||||
|
WH_URL=
|
||||||
|
```
|
||||||
|
|
||||||
|
| Property | Value | Examples |
|
||||||
|
|----------------|--------------------------------------------------------------------|----------------------------------------------|
|
||||||
|
| `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` |
|
||||||
|
|
||||||
|
To have Watchtower monitor your other containers, add this to their `compose.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
labels:
|
||||||
|
- 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!
|
||||||
@@ -1,58 +1,66 @@
|
|||||||
---
|
---
|
||||||
|
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"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 __Objectives:__
|
||||||
|
- Install Swag
|
||||||
[SWAG](https://docs.linuxserver.io/general/swag/) is the core of this homelab. It’s 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).
|
- Enable SSL
|
||||||
|
- Access the dashboard
|
||||||
::warning{to="/serveex/core/wireguard"}
|
- Configure regional blocking
|
||||||
|
- Expose Dockge
|
||||||
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 don’t 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, the dashboard mod for managing swag, fail2ban, and geolocation, and the auto-reload mod, which automatically reloads nginx whenever a config file changes, without having to restart the container.
|
[Swag](https://docs.linuxserver.io/general/swag/) is the core of this homelab. It’s 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"}
|
||||||
|
:::list{type="warning"}
|
||||||
|
- 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 don’t want to expose your services and prefer to always use a VPN to connect remotely, you can go [here instead](/serveex/security/wireguard).
|
||||||
|
:::
|
||||||
|
::
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
**Reverse proxy principle and its application in our case:**
|
**Reverse proxy principle and its application in our case:**
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
---
|
||||||
|
|
||||||
::note
|
::alert{type="info" icon="exclamation-circle"}
|
||||||
|
:::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-tree
|
File structure to be modified:
|
||||||
---
|
|
||||||
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.yaml
|
|
||||||
- .env
|
|
||||||
---
|
|
||||||
::
|
|
||||||
|
|
||||||
::steps{level="3"}
|
```sh
|
||||||
### Deploy the stack
|
root
|
||||||
|
└── 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 [compose.yaml]
|
```yaml
|
||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
swag:
|
swag:
|
||||||
@@ -72,7 +80,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:
|
||||||
- /srv/docker/swag/config:/config
|
- /docker/swag/config:/config
|
||||||
ports:
|
ports:
|
||||||
- 80:80
|
- 80:80
|
||||||
- 443:443
|
- 443:443
|
||||||
@@ -86,12 +94,11 @@ networks:
|
|||||||
name: swag_default
|
name: swag_default
|
||||||
```
|
```
|
||||||
|
|
||||||
::tip{icon=""}
|
::alert{type="success"}
|
||||||
✨ __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 [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
swag:
|
swag:
|
||||||
#...
|
#...
|
||||||
@@ -100,11 +107,9 @@ services:
|
|||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
### Set your environment variables
|
|
||||||
|
|
||||||
Then in the `.env` file:
|
Then in the `.env` file:
|
||||||
|
|
||||||
```properties [.env]
|
```properties
|
||||||
DOMAIN=
|
DOMAIN=
|
||||||
DOMAINS=
|
DOMAINS=
|
||||||
EMAIL=
|
EMAIL=
|
||||||
@@ -118,26 +123,24 @@ 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 generation, depends 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` |
|
||||||
|
|
||||||
### Configure the OVH DNS plugin
|
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.
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
::tip{icon="" to="/serveex/files/file-browser-quantum"}
|
::alert{type="success"}
|
||||||
✨ __Tip for terminal-shy users:__
|
✨ __Tip for terminal-shy users:__
|
||||||
You can use **File Browser Quantum** to browse and edit files instead of using terminal commands.
|
You can use [File Browser](/serveex/files/file-browser) to browse and edit files instead of using terminal commands.
|
||||||
::
|
::
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/dns-conf/ovh.ini
|
sudo vi /docker/swag/config/dns-conf/ovh.ini
|
||||||
```
|
```
|
||||||
|
|
||||||
You should see:
|
You should see:
|
||||||
|
|
||||||
```properties [ovh.ini]
|
```properties
|
||||||
# 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
|
||||||
@@ -155,54 +158,49 @@ 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 nano, just start typing, then :kbd{value="Ctrl+O"}, :kbd{value="Enter"}, :kbd{value="Ctrl+X"} to save and exit)
|
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)
|
||||||
|
|
||||||
Save with :kbd{value="Ctrl+O"}, then :kbd{value="Enter"}, and exit with :kbd{value="Ctrl+X"}.
|
Save and exit the file.
|
||||||
|
|
||||||
### 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
|
||||||
|
sudo vi /docker/swag/config/nginx/nginx.conf
|
||||||
```bash [Terminal]
|
|
||||||
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.conf]
|
```nginx
|
||||||
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).
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|
||||||
## 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, we’ll configure it to block a list of countries known to be the source of most malicious traffic. We’ll also configure a variable to allow internal server traffic, your box’s 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, we’ll configure it to block a list of countries known to be the source of most malicious traffic. We’ll also configure a variable to allow internal server traffic, your box’s 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).
|
||||||
|
|
||||||
::steps{level="3"}
|
Open `dbip.conf`:
|
||||||
### Open dbip.conf
|
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/nginx/dbip.conf
|
sudo vi /docker/swag/config/nginx/dbip.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
### Make your changes
|
Make your changes ([see documentation](https://github.com/linuxserver/docker-mods/tree/swag-dbip)), or use the following example:
|
||||||
|
|
||||||
Refer to the [documentation](https://github.com/linuxserver/docker-mods/tree/swag-dbip), or use the following example:
|
```nginx
|
||||||
|
|
||||||
```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;
|
||||||
@@ -246,65 +244,48 @@ geo $lan-ip {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Save and restart
|
Save and close the file. Restart the stack.
|
||||||
|
|
||||||
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 [some-app.subdomain.conf]{11}
|
```nginx
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## (Optional) Exposing Dockge
|
## 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`. 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).
|
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).
|
||||||
::
|
::
|
||||||
|
|
||||||
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`.
|
||||||
|
|
||||||
::warning
|
::alert{type="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 [TinyAuth](/serveex/security/tinyauth) or [Authentik](/serveex/advanced/authentik/). Otherwise, don’t expose it with SWAG. Use a VPN like [WireGuard](/serveex/core/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 [Authentik](/serveex/security/authentik/). Otherwise, don’t expose it with SWAG — use a VPN like [Wireguard](/serveex/security/wireguard) instead.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Create the subdomain.conf file
|
|
||||||
|
|
||||||
Open the `dockge.subdomain.conf` file:
|
Open the `dockge.subdomain.conf` file:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/nginx/proxy-confs/dockge.subdomain.conf
|
sudo vi /docker/swag/config/nginx/proxy-confs/dockge.subdomain.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure it like this:
|
Configure it like this:
|
||||||
|
|
||||||
```nginx [dockge.subdomain.conf]
|
```nginx
|
||||||
## Version 2023/12/19
|
## Version 2023/12/19
|
||||||
|
|
||||||
server {
|
server {
|
||||||
@@ -346,23 +327,20 @@ server {
|
|||||||
|
|
||||||
Save and exit. The configuration will update within a few seconds.
|
Save and exit. The configuration will update within a few seconds.
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
By default, SWAG doesn’t recognize the name "dockge". You’ll need to add Dockge’s network to SWAG’s `compose.yaml`.
|
- By default, SWAG doesn’t recognize the name "dockge". You’ll need to add Dockge’s network to SWAG’s `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 [compose.yaml]
|
```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
|
||||||
@@ -372,33 +350,31 @@ networks: # Define the custom network
|
|||||||
external: true
|
external: true
|
||||||
```
|
```
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::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.
|
||||||
|
|
||||||
### 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).
|
||||||
|
|
||||||
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:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
cd /srv/docker/swag/config/nginx/proxy-confs
|
cd /docker/swag/config/proxy-confs
|
||||||
sudo cp servicename.subdomain.conf.sample servicename.subdomain.conf
|
sudo cp servicename.subdomain.conf.sample servicename.subdomain.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
::caution
|
::alert{type="danger"}
|
||||||
|
:::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 SWAG’s `compose.yaml`
|
- Make sure you added the container's network in SWAG’s `compose.yml`
|
||||||
::
|
::
|
||||||
|
|
||||||
You can also customize the subdomain by editing `server_name yoursubdomain.*;`{lang=nginx} and renaming the file to `yoursubdomain.subdomain.conf`.
|
You can also customize the subdomain by editing `server_name yoursubdomain.*;`{lang=nginx} and renaming the file to `yoursubdomain.subdomain.conf`.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
navigation.title: Server core
|
||||||
|
icon: lucide:server-cog
|
||||||
+92
-122
@@ -1,18 +1,28 @@
|
|||||||
---
|
---
|
||||||
title: WireGuard
|
navigation: true
|
||||||
|
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"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 __Goals:__
|
||||||
|
- Install Wireguard
|
||||||
|
- Configure clients
|
||||||
|
- Access the secure network
|
||||||
|
::
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
---
|
||||||
Using a VPN allows remote access to a server’s local resources without exposing them to the internet. It’s 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 server’s local resources without exposing them to the internet. It’s 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:
|
||||||
|
|
||||||
- [wg-easy](https://github.com/wg-easy/wg-easy) as the server, providing a very simple web UI to manage connections and download config files (including QR codes for phones)
|
- [wg-easy](https://github.com/wg-easy/wg-easy) as the server, providing a very simple web UI to manage connections and download config files (including QR codes for phones)
|
||||||
- [WireGuard](https://docs.linuxserver.io/images/docker-wireguard/?h=wireguard) as the client for Linux systems
|
- [Wireguard](https://docs.linuxserver.io/images/docker-wireguard/?h=wireguard) as the client for Linux systems
|
||||||
|
|
||||||
Clients are also available for Windows, macOS, iOS, and Android.
|
Clients are also available for Windows, macOS, iOS, and Android.
|
||||||
|
|
||||||
@@ -27,7 +37,7 @@ To achieve this, we’ll create a **Virtual Private Network** (VPN), i.e., a sec
|
|||||||
|
|
||||||
Additionally, you can add your phone, laptop, or other devices to the VPN and securely access your server resources wherever you are.
|
Additionally, you can add your phone, laptop, or other devices to the VPN and securely access your server resources wherever you are.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
In this diagram, machine 1 is part of two networks:
|
In this diagram, machine 1 is part of two networks:
|
||||||
|
|
||||||
@@ -39,46 +49,41 @@ 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.
|
||||||
::
|
::
|
||||||
|
|
||||||
::warning
|
::alert{type="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), you’ll 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), you’ll need to use an external VPS and connect your local server as a client.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
### Folder Structure
|
### Folder Structure
|
||||||
|
|
||||||
::file-tree
|
```sh
|
||||||
---
|
root
|
||||||
tree:
|
└── docker
|
||||||
/:
|
└── wg-easy
|
||||||
- srv:
|
├── config
|
||||||
- docker:
|
│ └── etc_wireguard
|
||||||
- wg-easy:
|
├── compose.yaml
|
||||||
- etc_wireguard/
|
└── .env
|
||||||
- compose.yaml
|
```
|
||||||
- .env
|
|
||||||
---
|
|
||||||
::
|
|
||||||
|
|
||||||
::steps{level="3"}
|
Open Dockge, click **Compose**, and name the stack `wg_easy`.
|
||||||
### Deploy the stack
|
|
||||||
|
|
||||||
Open Dockge, click **Compose**, and name the stack `wg-easy`.
|
|
||||||
|
|
||||||
Copy the following configuration:
|
Copy the following configuration:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
wg-easy:
|
wg-easy:
|
||||||
environment:
|
environment:
|
||||||
- INSECURE=true # disables the web UI's own HTTPS; fine on a LAN-only or SWAG-fronted setup, remove it otherwise
|
- INSECURE=true
|
||||||
image: ghcr.io/wg-easy/wg-easy:15
|
image: ghcr.io/wg-easy/wg-easy:15
|
||||||
container_name: wg-easy
|
container_name: wg-easy
|
||||||
networks:
|
networks:
|
||||||
@@ -113,14 +118,12 @@ networks:
|
|||||||
- subnet: fdcc:ad94:bacf:61a3::/64
|
- subnet: fdcc:ad94:bacf:61a3::/64
|
||||||
```
|
```
|
||||||
|
|
||||||
::tip{icon=""}
|
::alert{type="success"}
|
||||||
✨ **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 [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
wg-easy:
|
wg-easy:
|
||||||
# ...
|
# ...
|
||||||
@@ -131,107 +134,76 @@ 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`.
|
||||||
|
|
||||||
::caution
|
::alert{type="danger"}
|
||||||
|
:::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 the default `0.0.0.0/0` (full tunnel) to `10.8.0.0/24` for **split tunneling**.
|
- Remove IPv6 (it often causes unnecessary issues).
|
||||||
- Optionally remove the IPv6 entry from `Allowed IPs` if you don't want client IPv6 traffic routed through the tunnel. This only affects what clients tunnel, not the container's own IPv6 setup above.
|
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
### Retrieve Configuration Files
|
### Retrieve Configuration Files
|
||||||
|
|
||||||
To configure clients:
|
To configure clients:
|
||||||
|
1. Access the web UI: `http://server-ip:51821`
|
||||||
::steps{level="4"}
|
2. Create a new client
|
||||||
#### Access the web UI
|
3. Edit the client and add `10.8.0.0/24` to `Server Allowed IPs`
|
||||||
|
4. (Optional) Set `Persistent Keep Alive` to `25` if it’s a permanently connected client
|
||||||
Go to `http://server-ip:51821`.
|
5. Save, download, and rename the file to `wg0.conf` (or `wg1.conf`, etc.)
|
||||||
|
|
||||||
#### 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 it’s 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"}
|
||||||
We assume the client server runs Linux with Docker installed.
|
:::list{type="info"}
|
||||||
|
- We assume the client server runs Linux with Docker installed.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
### Folder Structure
|
### Folder Structure
|
||||||
|
|
||||||
::file-tree
|
```sh
|
||||||
---
|
root
|
||||||
tree:
|
└── docker
|
||||||
/:
|
└── wireguard
|
||||||
- srv:
|
└── config
|
||||||
- docker:
|
│ └── wg_confs
|
||||||
- wireguard:
|
└── compose.yaml
|
||||||
- config:
|
|
||||||
- wg_confs/
|
|
||||||
- compose.yaml
|
|
||||||
---
|
|
||||||
::
|
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Create the folder
|
|
||||||
|
|
||||||
```bash [Terminal]
|
|
||||||
sudo mkdir -p /srv/docker/wireguard/config/wg_confs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
::tip{icon="" to="/serveex/files/file-browser-quantum"}
|
Create the folder:
|
||||||
✨ **Tip:** You can use **File Browser Quantum** instead of the terminal to edit and upload files.
|
|
||||||
::
|
|
||||||
|
|
||||||
### Create the wg0.conf file
|
```sh
|
||||||
|
sudo mkdir -p /docker/wireguard/config/wg_confs
|
||||||
```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"}.
|
::alert{type="success"}
|
||||||
|
✨ **Tip:** You can use [File Browser](/serveex/files/file-browser) instead of the terminal to edit and upload files.
|
||||||
|
::
|
||||||
|
|
||||||
::tip{icon=""}
|
Create the `wg0.conf` file:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo vi /docker/wireguard/config/wg_confs/wg0.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Enter insert mode (`i`), paste the downloaded configuration, then save (`Esc` → `:x`).
|
||||||
|
|
||||||
|
::alert{type="success"}
|
||||||
✨ **Alternative method:** Transfer the file via SFTP and move it:
|
✨ **Alternative method:** Transfer the file via SFTP and move it:
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo cp ~/wg0.conf /srv/docker/wireguard/config/wg_confs
|
sudo cp ~/wg0.conf /docker/wireguard/config/wg_confs
|
||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
### Create the compose file
|
Create the `compose.yaml` file in `/docker/wireguard`:
|
||||||
|
|
||||||
Create the `compose.yaml` file in `/srv/docker/wireguard`:
|
```yaml
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
wireguard:
|
wireguard:
|
||||||
image: lscr.io/linuxserver/wireguard:latest
|
image: lscr.io/linuxserver/wireguard:latest
|
||||||
@@ -243,37 +215,35 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/wireguard/config:/config
|
- /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
|
||||||
```bash [Terminal]
|
cd /docker/wireguard
|
||||||
cd /srv/docker/wireguard
|
|
||||||
sudo docker compose up -d
|
sudo docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### Done!
|
::alert{type="info"}
|
||||||
::
|
:::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.
|
||||||
|
|
||||||
::warning
|
::alert{type="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 here’s the final setup overview:
|
And here’s the final setup overview:
|
||||||
|
|
||||||

|

|
||||||
+149
-238
@@ -1,14 +1,18 @@
|
|||||||
---
|
---
|
||||||
|
navigation: true
|
||||||
title: Authentik
|
title: Authentik
|
||||||
description: Install Authentik as a self-hosted identity provider, configure MFA and protect your services with SSO and reverse proxy authentication.
|
description: Install Authentik as a self-hosted identity provider — configure MFA and protect your services with SSO and reverse proxy authentication.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
---
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
# Authentik
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 __Objectives:__
|
||||||
|
- Install and expose Authentik
|
||||||
::note
|
- Configure Multi-Factor Authentication (MFA)
|
||||||
|
- Protect a native app or an app behind a reverse proxy
|
||||||
This is an advanced alternative to [TinyAuth](/serveex/security/tinyauth) and [Pocket ID](/serveex/security/pocket-id): a single application handling both the forward-auth proxy and the OIDC provider roles, with built-in LDAP, SAML, and RBAC-style access policies (restricting who can reach which app, not just whether they're logged in) on top of its own admin UI for users, groups, and outposts. TinyAuth and Pocket ID stay lighter and simpler for a small homelab; Authentik is worth the extra setup once you need finer-grained access control or broader protocol support than OIDC alone.
|
|
||||||
::
|
::
|
||||||
|
|
||||||
[Authentik](https://goauthentik.io) is a single sign-on (SSO) tool that allows you to log in once to all platforms compatible with OpenID. It can also secure access to your exposed services by injecting itself via SWAG into requests to those services.
|
[Authentik](https://goauthentik.io) is a single sign-on (SSO) tool that allows you to log in once to all platforms compatible with OpenID. It can also secure access to your exposed services by injecting itself via SWAG into requests to those services.
|
||||||
@@ -19,68 +23,60 @@ Authentik also supports multi-factor authentication, including TOTP (a code gene
|
|||||||
|
|
||||||
It's a great alternative to VPNs for securely exposing services, especially ones that lack MFA or login protection (e.g., the SWAG dashboard).
|
It's a great alternative to VPNs for securely exposing services, especially ones that lack MFA or login protection (e.g., the SWAG dashboard).
|
||||||
|
|
||||||
Authentik has [extensive documentation](https://docs.goauthentik.io/install-config/install/docker-compose) and [great tutorials from Cooptonian](https://www.youtube.com/@cooptonian). Here, we’ll cover the basics using Dockge as an example.
|
Authentik has [extensive documentation](https://docs.goauthentik.io/docs/installation/docker-compose) and [great tutorials from Cooptonian](https://www.youtube.com/@cooptonian). Here, we’ll cover the basics using Dockge as an example.
|
||||||
|
|
||||||
There are two main modes you should know:
|
There are two main modes you should know:
|
||||||
|
|
||||||
- The first allows apps with native support for OpenID-compatible SSO to connect directly to Authentik. This is the preferred method, as the app itself decides what’s public and what’s protected.
|
- The first allows apps with native support for OpenID-compatible SSO to connect directly to Authentik. This is the preferred method, as the app itself decides what’s public and what’s protected.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- The second method injects Authentik authentication through SWAG before reaching the target service.
|
- The second method injects Authentik authentication through SWAG before reaching the target service.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
Both modes can be configured on a per-application basis.
|
Both modes can be configured on a per-application basis.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
::file-tree
|
|
||||||
---
|
---
|
||||||
tree:
|
Folder structure:
|
||||||
/:
|
```sh
|
||||||
- srv:
|
root
|
||||||
- docker:
|
└── docker
|
||||||
- authentik:
|
└── authentik
|
||||||
- .env
|
├── .env
|
||||||
- compose.yaml
|
├── compose.yml
|
||||||
- media/
|
├── media
|
||||||
- certs/
|
├── certs
|
||||||
- custom-template/
|
├── custom-template
|
||||||
- ssh/
|
└── ssh
|
||||||
---
|
|
||||||
::
|
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Create the folders
|
|
||||||
|
|
||||||
```bash [Terminal]
|
|
||||||
sudo mkdir -p /srv/docker/authentik/media /srv/docker/authentik/certs /srv/docker/authentik/custom-template /srv/docker/authentik/ssh
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Generate secrets
|
Create the folders:
|
||||||
|
|
||||||
Navigate to the `authentik` folder via `cd /srv/docker/authentik` and generate a password and secret key to include in the `.env` file:
|
```sh
|
||||||
|
sudo mkdir -p /docker/authentik/media /docker/authentik/certs /docker/authentik/custom-template /docker/authentik/ssh
|
||||||
```bash [Terminal]
|
|
||||||
echo "PG_PASS=$(openssl rand 36 | base64)" | sudo tee -a .env
|
|
||||||
echo "AUTHENTIK_SECRET_KEY=$(openssl rand 60 | base64)" | sudo tee -a .env
|
|
||||||
```
|
```
|
||||||
|
|
||||||
::note
|
Navigate to the `authentik` folder via `cd /docker/authentik` and generate a password and secret key to include in the `.env` file:
|
||||||
|
|
||||||
To generate the keys, we created the folders ahead of deployment using Dockge. Dockge will prevent you from creating a stack with the same name in these folders unless a `compose.yaml` file exists. So, create an empty `compose.yaml` so it appears as an inactive stack:
|
```sh
|
||||||
```bash [Terminal]
|
sudo echo "PG_PASS=$(openssl rand 36 | base64)" >> .env
|
||||||
sudo nano /srv/docker/authentik/compose.yaml
|
sudo echo "AUTHENTIK_SECRET_KEY=$(openssl rand 60 | base64)" >> .env
|
||||||
```
|
```
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
|
- To generate the keys, we created the folders ahead of deployment using Dockge. Dockge will prevent you from creating a stack with the same name in these folders unless a `compose.yml` file exists. So, create an empty `compose.yml` so it appears as an inactive stack:
|
||||||
|
:::
|
||||||
|
```sh
|
||||||
|
sudo vi /docker/authentik/compose.yml
|
||||||
::
|
::
|
||||||
|
|
||||||
### Deploy the stack
|
|
||||||
|
|
||||||
Open Dockge and search for "authentik" in the inactive stacks.
|
Open Dockge and search for "authentik" in the inactive stacks.
|
||||||
Name the stack `authentik` and paste the following configuration, replacing `{AUTHENTIK_TAG:-2026.2}`{lang=properties} with [the latest version of Authentik](https://goauthentik.io/docs/releases).
|
Name the stack `authentik` and paste the following configuration, replacing `{AUTHENTIK_TAG:-2026.2}`{lang=properties} with [the latest version of Authentik](https://goauthentik.io/docs/releases).
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
|
|
||||||
@@ -156,54 +152,24 @@ services:
|
|||||||
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
|
||||||
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
|
||||||
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
|
||||||
# `user: root` and the Docker integration below are optional, only needed if you
|
# `user: root` and the docker socket volume are optional.
|
||||||
# want Authentik to auto-manage embedded outposts on this host. See:
|
# See more for the docker socket integration here:
|
||||||
# https://goauthentik.io/docs/outposts/integrations/docker
|
# https://goauthentik.io/docs/outposts/integrations/docker
|
||||||
# Removing `user: root` also prevents the worker from fixing the permissions
|
# Removing `user: root` also prevents the worker from fixing the permissions
|
||||||
# on the mounted folders, so when removing this make sure the folders have the correct UID/GID
|
# on the mounted folders, so when removing this make sure the folders have the correct UID/GID
|
||||||
# (1000:1000 by default)
|
# (1000:1000 by default)
|
||||||
user: root
|
user: root
|
||||||
volumes:
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
- ./media:/media
|
- ./media:/media
|
||||||
- ./certs:/certs
|
- ./certs:/certs
|
||||||
- ./custom-templates:/templates
|
- ./custom-templates:/templates
|
||||||
- ./ssh:/authentik/.ssh
|
- ./ssh:/authentik/.ssh
|
||||||
networks:
|
|
||||||
- default
|
|
||||||
- authentik-internal
|
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
depends_on:
|
depends_on:
|
||||||
- postgresql
|
- postgresql
|
||||||
- redis
|
- redis
|
||||||
- docker-socket-proxy
|
|
||||||
|
|
||||||
docker-socket-proxy:
|
|
||||||
image: lscr.io/linuxserver/socket-proxy:latest
|
|
||||||
container_name: docker-socket-proxy-authentik
|
|
||||||
security_opt:
|
|
||||||
- no-new-privileges:true
|
|
||||||
networks:
|
|
||||||
- authentik-internal
|
|
||||||
volumes:
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
|
||||||
environment:
|
|
||||||
- CONTAINERS=1
|
|
||||||
- IMAGES=1
|
|
||||||
- NETWORKS=1
|
|
||||||
- INFO=1
|
|
||||||
- POST=1
|
|
||||||
- ALLOW_START=1
|
|
||||||
- ALLOW_STOP=1
|
|
||||||
- ALLOW_RESTARTS=1
|
|
||||||
restart: unless-stopped
|
|
||||||
read_only: true
|
|
||||||
tmpfs:
|
|
||||||
- /run
|
|
||||||
|
|
||||||
networks:
|
|
||||||
authentik-internal:
|
|
||||||
name: authentik-internal
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
database:
|
database:
|
||||||
@@ -212,68 +178,56 @@ volumes:
|
|||||||
driver: local
|
driver: local
|
||||||
```
|
```
|
||||||
|
|
||||||
::note
|
|
||||||
This adds **Docker Socket Proxy** so the optional Docker integration never needs `/var/run/docker.sock` mounted directly into the worker. If you enable it, set the connection's Docker URL in the admin UI to `http://docker-socket-proxy:2375` instead of the local socket path, as [Authentik's own docs recommend](https://goauthentik.io/docs/outposts/integrations/docker) for socket-proxy setups.
|
|
||||||
::
|
|
||||||
|
|
||||||
### Begin the initial setup
|
|
||||||
|
|
||||||
In the `.env` file, the `PG_PASS` and `AUTHENTIK_SECRET_KEY` variables are already set.
|
In the `.env` file, the `PG_PASS` and `AUTHENTIK_SECRET_KEY` variables are already set.
|
||||||
Deploy the stack.
|
Deploy the stack.
|
||||||
|
|
||||||
You can then begin the initial setup by visiting:
|
You can then begin the initial setup by visiting:
|
||||||
`http://yourserverip:9000/if/flow/initial-setup/`
|
`http://yourserverip:9000/if/flow/initial-setup/`
|
||||||
|
|
||||||
::warning
|
::alert{type="warning"}
|
||||||
|
:::list{type="warning"}
|
||||||
__Warning:__ It’s recommended to create a new admin account and **disable** the default `akadmin` account.
|
- __Warning:__ It’s recommended to create a new admin account and **disable** the default `akadmin` account.
|
||||||
::
|
:::
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
::
|
||||||
|
|
||||||
## Exposing Authentik
|
## Exposing Authentik
|
||||||
|
---
|
||||||
To use Authentik outside your local network, you must expose it.
|
To use Authentik outside your local network, you must expose it.
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
📋 __Prerequisites:__ <br/><br/>
|
📋 __Prerequisites:__ <br/><br/>
|
||||||
We assume you have already created a subdomain like `auth.mydomain.com` in your [DNS zone](/general/networking/dns), with a CNAME pointing to `mydomain.com`. Also, unless you're using [Cloudflare Zero Trust](/serveex/security/cloudflare), you must have already forwarded port `443` from your router to port `443` of your server in your [NAT rules](/general/networking/nat).
|
We assume you have already created a subdomain like `auth.mydomain.com` in your [DNS zone](/general/networking/dns), with a CNAME pointing to `mydomain.com`. Also, unless you're using [Cloudflare Zero Trust](/serveex/security/cloudflare), you must have already forwarded port `443` from your router to port `443` of your server in your [NAT rules](/general/networking/nat).
|
||||||
::
|
::
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Verify authentik-server.conf
|
|
||||||
|
|
||||||
Open the `authentik-server.conf` file:
|
Open the `authentik-server.conf` file:
|
||||||
|
|
||||||
::tip{icon="" to="/serveex/files/file-browser-quantum"}
|
::alert{type="success"}
|
||||||
✨ __Tip for those who dislike terminals:__
|
✨ __Tip for those who dislike terminals:__
|
||||||
You can use **File Browser Quantum** to navigate and edit files instead of using terminal commands.
|
You can use [File Browser](/serveex/files/file-browser) to navigate and edit files instead of using terminal commands.
|
||||||
::
|
::
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/nginx/authentik-server.conf
|
sudo vi /docker/swag/config/nginx/authentik-server.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify that the following variables are set correctly:
|
Verify that the following variables are set correctly:
|
||||||
|
|
||||||
```nginx [authentik-server.conf]
|
```nginx
|
||||||
set $upstream_authentik authentik-server;
|
set $upstream_authentik authentik-server;
|
||||||
proxy_pass http://$upstream_authentik:9000;
|
proxy_pass http://$upstream_authentik:9000;
|
||||||
```
|
```
|
||||||
|
|
||||||
If not, make the necessary changes, then save with :kbd{value="Ctrl+O"}, :kbd{value="Enter"}, and exit with :kbd{value="Ctrl+X"}.
|
If not, press `i` to enter edit mode, make the necessary changes, then save and exit by pressing `Esc` followed by `:x`.
|
||||||
|
|
||||||
### Create the subdomain.conf file
|
|
||||||
|
|
||||||
Create the `auth.subdomain.conf` file:
|
Create the `auth.subdomain.conf` file:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/nginx/proxy-confs/auth.subdomain.conf
|
sudo vi /docker/swag/config/nginx/proxy-confs/auth.subdomain.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
Paste the following configuration:
|
Press `i` to enter edit mode and paste the following configuration:
|
||||||
|
|
||||||
```nginx [auth.subdomain.conf]
|
```nginx
|
||||||
## Version 2023/05/31
|
## Version 2023/05/31
|
||||||
# Ensure your authentik container is named authentik-server
|
# Ensure your authentik container is named authentik-server
|
||||||
# Ensure your DNS has a CNAME for authentik
|
# Ensure your DNS has a CNAME for authentik
|
||||||
@@ -308,19 +262,16 @@ server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Save with :kbd{value="Ctrl+O"}, then :kbd{value="Enter"}, and exit with :kbd{value="Ctrl+X"}.
|
Save and exit by pressing `Esc` then `:x`.
|
||||||
|
|
||||||
### Add Authentik's network to SWAG
|
|
||||||
|
|
||||||
Go to Dockge, and edit the SWAG compose file to add the Authentik network:
|
Go to Dockge, and edit the SWAG compose file to add the Authentik network:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
swag:
|
swag:
|
||||||
container_name: # ...
|
container_name: # ...
|
||||||
# ...
|
# ...
|
||||||
networks: # Attach the container to the custom network
|
networks: # Attach the container to the custom network
|
||||||
# ...
|
# ...
|
||||||
- authentik # Name of the network declared in the stack
|
- authentik # Name of the network declared in the stack
|
||||||
|
|
||||||
@@ -333,32 +284,28 @@ networks: # Define the custom network
|
|||||||
|
|
||||||
Restart the stack and wait for SWAG to be fully operational.
|
Restart the stack and wait for SWAG to be fully operational.
|
||||||
|
|
||||||
### Done!
|
Done! You can now access Authentik via `https://auth.mydomain.com`
|
||||||
::
|
|
||||||
|
|
||||||
You can now access Authentik via `https://auth.mydomain.com`
|
|
||||||
|
|
||||||
## Enable Multifactor Authentication
|
## Enable Multifactor Authentication
|
||||||
|
---
|
||||||
The main value of Authentik is using multifactor authentication for all protected apps.
|
The main value of Authentik is using multifactor authentication for all protected apps.
|
||||||
|
|
||||||
::steps{level="3"}
|
- Go to `https://auth.mydomain.com`
|
||||||
### Log in to Authentik
|
- Log in
|
||||||
|
- Go to _Settings_
|
||||||
Go to `https://auth.mydomain.com` and log in.
|
- Click the _MFA_ section
|
||||||
|
- Click _Register_
|
||||||
### Register an MFA method
|
- Choose a method like _TOTP device_ (you'll need an authenticator app like Google Authenticator)
|
||||||
|
- Follow the steps
|
||||||
Go to _Settings_, click the _MFA_ section, then _Register_. Choose a method like _TOTP device_ (you'll need an authenticator app like Google Authenticator) and follow the steps.
|
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
You’ll now be prompted to enter a one-time code at every login.
|
You’ll now be prompted to enter a one-time code at every login.
|
||||||
|
|
||||||
## Protecting a Native App
|
## Protecting a Native App
|
||||||
Authentik is natively compatible with several applications. You can find the list and [support here](https://integrations.goauthentik.io/).
|
---
|
||||||
|
Authentik is natively compatible with several applications. You can find the list and [support here](https://docs.goauthentik.io/integrations/services/).
|
||||||
|
|
||||||
## Protecting an App via Reverse Proxy
|
## Protecting an App via Reverse Proxy
|
||||||
|
---
|
||||||
SWAG lets you insert Authentik’s login page between a request and access to your service. To do this:
|
SWAG lets you insert Authentik’s login page between a request and access to your service. To do this:
|
||||||
|
|
||||||
- Configure the authentication provider in Authentik.
|
- Configure the authentication provider in Authentik.
|
||||||
@@ -366,7 +313,6 @@ SWAG lets you insert Authentik’s login page between a request and access to yo
|
|||||||
|
|
||||||
Why do this when Dockge already has authentication? Because Dockge uses weak HTTP authentication. With Authentik, you get strong MFA authentication and automatic login to all apps protected by Authentik. This secures access to Dockge and other apps without needing a VPN.
|
Why do this when Dockge already has authentication? Because Dockge uses weak HTTP authentication. With Authentik, you get strong MFA authentication and automatic login to all apps protected by Authentik. This secures access to Dockge and other apps without needing a VPN.
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Configuring Authentik
|
### Configuring Authentik
|
||||||
|
|
||||||
- Go to Authentik
|
- Go to Authentik
|
||||||
@@ -374,15 +320,15 @@ Why do this when Dockge already has authentication? Because Dockge uses weak HTT
|
|||||||
- Select _Applications_ then _Create with wizard_
|
- Select _Applications_ then _Create with wizard_
|
||||||
- Fill in the fields as shown:
|
- Fill in the fields as shown:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- At the next step, choose "Forward authentication (single application)" and configure it as shown (flows are important):
|
- At the next step, choose "Forward authentication (single application)" and configure it as shown (flows are important):
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- Next, go to the _Outposts_ menu on the left and edit _authentik Embedded Outpost_:
|
- Next, go to the _Outposts_ menu on the left and edit _authentik Embedded Outpost_:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- Add the `dockge` application by moving it to the right column and save.
|
- Add the `dockge` application by moving it to the right column and save.
|
||||||
|
|
||||||
@@ -390,52 +336,49 @@ Why do this when Dockge already has authentication? Because Dockge uses weak HTT
|
|||||||
|
|
||||||
Edit the file `dockge.mydomain.com`:
|
Edit the file `dockge.mydomain.com`:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/nginx/proxy-confs/dockge.subdomain.conf
|
sudo vi /docker/swag/config/nginx/proxy-confs/dockge.subdomain.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
Uncomment the two lines `#include /config/nginx/authentik-server.conf;`
|
Press `i` to enter edit mode and uncomment the two lines `#include /config/nginx/authentik-server.conf;`
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Esc`, type `:x`, and press `Enter` to save and exit.
|
||||||
|
|
||||||
::tip{icon=""}
|
Done! Now when accessing `https://dockge.mydomain.com`, you’ll be redirected to the Authentik login screen.
|
||||||
|
|
||||||
|
::alert{type="success"}
|
||||||
✨ __Tip:__ In Dockge's settings, you can disable Dockge's authentication to avoid double login. **Warning**: this means if the port is open on your local network, there will be no authentication at all.
|
✨ __Tip:__ In Dockge's settings, you can disable Dockge's authentication to avoid double login. **Warning**: this means if the port is open on your local network, there will be no authentication at all.
|
||||||
::
|
::
|
||||||
|
|
||||||
### Done!
|
::alert{type="info"}
|
||||||
::
|
:::list{type="info"}
|
||||||
|
- Repeat this process for each app you want to protect (unless it has native integration with Authentik).
|
||||||
Now when accessing `https://dockge.mydomain.com`, you’ll be redirected to the Authentik login screen.
|
:::
|
||||||
|
|
||||||
::note
|
|
||||||
|
|
||||||
Repeat this process for each app you want to protect (unless it has native integration with Authentik).
|
|
||||||
::
|
::
|
||||||
|
|
||||||
Your new architecture looks like this:
|
Your new architecture looks like this:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Protecting a Remote Server Service
|
## Protecting a Remote Server Service
|
||||||
For a [native application](/serveex/advanced/authentik/#protecting-a-native-app) (via OAuth 2.0 or other), nothing changes.
|
---
|
||||||
|
For a [native application](/serveex/security/authentik/#protecting-a-native-app) (via OAuth 2.0 or other), nothing changes.
|
||||||
|
|
||||||
For a non-native app behind a reverse proxy, you must deploy an __Outpost__. An Outpost is a container acting as a local proxy. It's the target of your app's auth requests and the only one authorized to communicate with your Authentik API.
|
For a non-native app behind a reverse proxy, you must deploy an __Outpost__. An Outpost is a container acting as a local proxy — it's the target of your app's auth requests and the only one authorized to communicate with your Authentik API.
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
Prerequisites:
|
Prerequisites:
|
||||||
|
|
||||||
- Install [Docker](/serveex/core/docker) on the remote server hosting the service.
|
- Install [Docker](/serveex/core/docker) on the remote server hosting the service.
|
||||||
- If the app has no native integration, use a compatible reverse proxy. We will use [SWAG](/serveex/core/swag) here.
|
- If the app has no native integration, use a compatible reverse proxy. We will use [SWAG](/serveex/core/swag) here.
|
||||||
::
|
::
|
||||||
|
|
||||||
This container will forward requests to your main [Authentik](/serveex/advanced/authentik) instance over the internet (or your local network). The server will perform checks and respond to the Outpost, which will allow or block access accordingly.
|
This container will forward requests to your main [Authentik](/serveex/security/authentik#authentik) instance over the internet (or your local network). The server will perform checks and respond to the Outpost, which will allow or block access accordingly.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Configuring Authentik
|
### Configuring Authentik
|
||||||
|
|
||||||
Create your [providers and applications](/serveex/advanced/authentik/#protecting-a-native-app) as shown earlier.
|
Create your [providers and applications](/serveex/security/authentik/#protecting-a-native-app) as shown earlier.
|
||||||
|
|
||||||
Then, in the admin panel, go to _Applications > Outposts_, and create a new outpost.
|
Then, in the admin panel, go to _Applications > Outposts_, and create a new outpost.
|
||||||
|
|
||||||
@@ -459,47 +402,46 @@ authentik_host_insecure: false
|
|||||||
container_image:
|
container_image:
|
||||||
docker_network: null
|
docker_network: null
|
||||||
docker_map_ports: true
|
docker_map_ports: true
|
||||||
|
docker_labels: null
|
||||||
```
|
```
|
||||||
|
|
||||||
Save with :kbd{value="Ctrl+O"}, then :kbd{value="Enter"}, and exit with :kbd{value="Ctrl+X"}.
|
Save and exit.
|
||||||
|
|
||||||
On the list of created outposts, locate the new one and click _Show details_ at the end of the line. Carefully copy the access token.
|
On the list of created outposts, locate the new one and click _Show details_ at the end of the line. Carefully copy the access token.
|
||||||
|
|
||||||
We assume you’ve already installed [Docker](/serveex/core/docker) and [SWAG](/serveex/core/swag) on this remote machine. Here's how to configure it:
|
|
||||||
|
|
||||||
### Create the stack folder
|
### Configuring the Remote Machine
|
||||||
|
|
||||||
On your remote machine, use [Dockge](/serveex/core/docker/#install-dockge-to-manage-and-deploy-containers) to create a stack named `authentik-outpost`.
|
We assume you’ve already installed [Docker](/serveex/core/docker) and [SWAG](/serveex/core/swag) on this remote machine.
|
||||||
|
|
||||||
If you haven’t installed [Dockge](/serveex/core/docker/#install-dockge-to-manage-and-deploy-containers), create a folder `/srv/docker/authentik-outpost`, or directly via command line:
|
On your remote machine, use [Dockge](/serveex/core/docker/#installer-dockge-pour-gérer-et-déployer-les-conteneurs) to create a stack named `authentik-outpost`.
|
||||||
|
|
||||||
```bash [Terminal]
|
If you haven’t installed [Dockge](/serveex/core/docker/#installer-dockge-pour-gérer-et-déployer-les-conteneurs), create a folder `/docker/authentik-outpost`, or directly via command line:
|
||||||
sudo mkdir -p /srv/docker/authentik-outpost
|
|
||||||
|
```sh
|
||||||
|
sudo mkdir -P /docker/authentik-outpost
|
||||||
```
|
```
|
||||||
|
|
||||||
::tip{icon="" to="/serveex/files/file-browser-quantum"}
|
::alert{type="success"}
|
||||||
✨ __Tip for terminal-averse users:__
|
✨ __Tip for terminal-averse users:__
|
||||||
You can use **File Browser Quantum** to navigate and edit your files instead of using terminal commands.
|
You can use [File Browser](/serveex/files/file-browser) to navigate and edit your files instead of using terminal commands.
|
||||||
::
|
::
|
||||||
|
|
||||||
### Create the compose file
|
|
||||||
|
|
||||||
Create the `compose.yaml` file or paste the configuration directly into Dockge if installed.
|
Create the `compose.yaml` file or paste the configuration directly into Dockge if installed.
|
||||||
|
|
||||||
Via command line:
|
Via command line:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/authentik-outpost/compose.yaml
|
sudo vi /docker/authentik-outpost/compose.yaml
|
||||||
```
|
```
|
||||||
Paste the following configuration, updating the version in `ghcr.io/goauthentik/proxy:2026.2`{lang=properties} to match your Authentik server version.
|
Enter edit mode by pressing `i` and paste the following configuration, updating the version in `{AUTHENTIK_TAG:proxy:2024.2.3}`{lang=properties} to match your Authentik server version.
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
version: "3.5"
|
version: "3.5"
|
||||||
services:
|
services:
|
||||||
authentik_proxy:
|
authentik_proxy:
|
||||||
container_name: authentik-outpost
|
container_name: authentik-outpost
|
||||||
image: ghcr.io/goauthentik/proxy:2026.2
|
image: ghcr.io/goauthentik/proxy:2024.2.3
|
||||||
# Optionally specify which networks the container should be
|
# Optionally specify which networks the container should be
|
||||||
# might be needed to reach the core authentik server
|
# might be needed to reach the core authentik server
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -514,22 +456,18 @@ services:
|
|||||||
AUTHENTIK_TOKEN: ${TOKEN}
|
AUTHENTIK_TOKEN: ${TOKEN}
|
||||||
```
|
```
|
||||||
|
|
||||||
### Add the outpost network to SWAG
|
|
||||||
|
|
||||||
Go to the SWAG stack on the remote machine (or edit directly using Dockge) and add the authentik-outpost network in the configuration file like this (see `networks` section):
|
Go to the SWAG stack on the remote machine (or edit directly using Dockge) and add the authentik-outpost network in the configuration file like this (see `networks` section):
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/compose.yaml
|
sudo vi /docker/swag/compose.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
swag:
|
swag:
|
||||||
container_name: #...
|
container_name: #...
|
||||||
# ...
|
# ...
|
||||||
networks: # Attach the container to the custom network
|
networks: # Attach the container to the custom network
|
||||||
|
|
||||||
- authentik-outpost # Network name as declared in the stack
|
- authentik-outpost # Network name as declared in the stack
|
||||||
|
|
||||||
networks: # Define the custom network
|
networks: # Define the custom network
|
||||||
@@ -539,37 +477,34 @@ networks: # Define the custom network
|
|||||||
external: true # Marks it as an external network
|
external: true # Marks it as an external network
|
||||||
```
|
```
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Esc`, then type `:x` and press `Enter` to save and exit.
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
We assume the Dockge network name is `authentik-outpost_default`.
|
- We assume the Dockge network name is `authentik-outpost_default`.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
### Restart SWAG
|
If using [Dockge](/serveex/core/docker/#installer-dockge-pour-gérer-et-déployer-les-conteneurs), restart SWAG.
|
||||||
|
|
||||||
If using [Dockge](/serveex/core/docker/#install-dockge-to-manage-and-deploy-containers), restart SWAG.
|
|
||||||
|
|
||||||
Otherwise, via terminal:
|
Otherwise, via terminal:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
cd /srv/docker/swag/
|
cd /docker/swag/
|
||||||
sudo docker compose up -d
|
sudo docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
### Set your environment variables
|
|
||||||
|
|
||||||
Create (or fill using Dockge) the `.env` file in the `authentik-outpost` directory:
|
Create (or fill using Dockge) the `.env` file in the `authentik-outpost` directory:
|
||||||
|
|
||||||
Via command line:
|
Via command line:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/authentik-outpost/.env
|
sudo vi /docker/authentik-outpost/.env
|
||||||
```
|
```
|
||||||
|
|
||||||
Paste the following configuration:
|
Enter edit mode with `i` and paste the following configuration:
|
||||||
|
|
||||||
```properties [.env]
|
```properties
|
||||||
HOST=
|
HOST=
|
||||||
TOKEN=
|
TOKEN=
|
||||||
```
|
```
|
||||||
@@ -581,84 +516,60 @@ Fill in the values:
|
|||||||
| `HOST`{lang=properties} | The URL of your Authentik server | `https://auth.domain.com` |
|
| `HOST`{lang=properties} | The URL of your Authentik server | `https://auth.domain.com` |
|
||||||
| `TOKEN`{lang=properties} | The previously copied access token | `Q2pVEqsTNRkJSO9SkJzU3KZ2` |
|
| `TOKEN`{lang=properties} | The previously copied access token | `Q2pVEqsTNRkJSO9SkJzU3KZ2` |
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Esc`, then type `:x` and press `Enter` to save and exit.
|
||||||
|
|
||||||
### Deploy the stack
|
|
||||||
|
|
||||||
If using Dockge, deploy the stack.
|
If using Dockge, deploy the stack.
|
||||||
|
|
||||||
Otherwise, via terminal:
|
Otherwise, via terminal:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
cd /srv/docker/authentik-outpost/
|
cd /docker/authentik-outpost/
|
||||||
sudo docker compose up -d
|
sudo docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
The container is now running. You can verify its status from your Authentik instance admin panel under _Applications > Outposts_.
|
The container is now running. You can verify its status from your Authentik instance admin panel under _Applications > Outposts_.
|
||||||
|
|
||||||
### Point SWAG at the outpost
|
|
||||||
|
|
||||||
Now, let’s configure SWAG.
|
Now, let’s configure SWAG.
|
||||||
|
|
||||||
Open the `authentik-server.conf` file:
|
Open the `authentik-server.conf` file:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/nginx/authentik-server.conf
|
sudo vi /docker/swag/config/nginx/authentik-server.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
In the file, change `authentik-server` to `authentik-outpost` as shown:
|
In the file, press `i` to enter edit mode and change `authentik-server` to `authentik-outpost` as shown:
|
||||||
|
|
||||||
```nginx [authentik-server.conf]
|
```nginx
|
||||||
set $upstream_authentik authentik-outpost;
|
set $upstream_authentik authentik-outpost;
|
||||||
proxy_pass http://$upstream_authentik:9000;
|
proxy_pass http://$upstream_authentik:9000;
|
||||||
```
|
```
|
||||||
|
|
||||||
Save with :kbd{value="Ctrl+O"}, then :kbd{value="Enter"}, and exit with :kbd{value="Ctrl+X"}.
|
Save and exit with `Esc`, then `:x` and `Enter`.
|
||||||
|
|
||||||
### Done!
|
Then configure the applications to protect as you did on your main server, whether they are [native](/serveex/security/authentik/#protecting-a-native-app) or protected via [reverse proxy](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
|
||||||
::
|
|
||||||
|
|
||||||
Then configure the applications to protect as you did on your main server, whether they are [native](/serveex/advanced/authentik/#protecting-a-native-app) or protected via [reverse proxy](/serveex/advanced/authentik#protecting-an-app-via-reverse-proxy).
|
|
||||||
|
|
||||||
## Migrating an Authentik Database
|
## Migrating an Authentik Database
|
||||||
|
---
|
||||||
::note
|
|
||||||
|
|
||||||
The commands below assume the Postgres container is named `authentik-postgresql`, matching the [Installation](#installation) section above. Adjust it if you renamed yours.
|
|
||||||
::
|
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Dump the database
|
|
||||||
|
|
||||||
On the source machine, dump the database:
|
On the source machine, dump the database:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo docker exec authentik-postgresql pg_dump -U authentik -F t authentik > /path/to/mydb.tar
|
sudo docker exec authentik-postgres pg_dump -U authentik -F t authentik > /path/to/mydb.tar
|
||||||
```
|
```
|
||||||
|
|
||||||
### Transfer the dump
|
Then transfer it to the target machine. On the target machine, copy the file into the Docker container:
|
||||||
|
|
||||||
Transfer it to the target machine, then copy the file into the Docker container:
|
```sh
|
||||||
|
cp /path/to/mydb.tar authentik-postgres:/path/to/wherever
|
||||||
```bash [Terminal]
|
|
||||||
cp /path/to/mydb.tar authentik-postgresql:/path/to/wherever
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Purge existing tables
|
|
||||||
|
|
||||||
(Optional) Purge existing tables:
|
(Optional) Purge existing tables:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo docker exec -i authentik-postgresql psql -U authentik -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'authentik' AND pid <> pg_backend_pid();" && sudo docker exec -i authentik-postgresql psql -U authentik -d postgres -c "DROP DATABASE IF EXISTS authentik;" && sudo docker exec -i authentik-postgresql psql -U authentik -d postgres -c "CREATE DATABASE authentik;"
|
sudo docker exec -i authentik-postgres psql -U authentik -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'authentik' AND pid <> pg_backend_pid();" && sudo docker exec -i authentik-postgres psql -U authentik -d postgres -c "DROP DATABASE IF EXISTS authentik;" && sudo docker exec -i authentik-postgres psql -U authentik -d postgres -c "CREATE DATABASE authentik;"
|
||||||
```
|
```
|
||||||
|
|
||||||
### Restore the database
|
|
||||||
|
|
||||||
Restore the database:
|
Restore the database:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo docker exec authentik-postgresql pg_restore -U authentik -d authentik /path/to/wherever/mydb.tar
|
sudo docker exec authentik-postgresql pg_restore -U authentik -d authentik /path/to/wherever/mydb.tar
|
||||||
```
|
```
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
|
||||||
+63
-69
@@ -1,16 +1,28 @@
|
|||||||
---
|
---
|
||||||
|
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}
|
|
||||||

|

|
||||||
|
|
||||||
## 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. It’s a broad concept, but we’ll 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. It’s a broad concept, but we’ll 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/advanced/authentik).
|
_Cloudflare tunnels_ offer a simple way to implement _Zero Trust_, using [SWAG](/serveex/core/swag) and [Authentik](/serveex/security/authentik).
|
||||||
|
|
||||||
Simply put, Cloudflare Tunnels allow you to:
|
Simply put, Cloudflare Tunnels allow you to:
|
||||||
|
|
||||||
@@ -22,14 +34,17 @@ Simply put, Cloudflare Tunnels allow you to:
|
|||||||
|
|
||||||
Here we’ll explain how to integrate SWAG with Cloudflare tunnels.
|
Here we’ll explain how to integrate SWAG with Cloudflare tunnels.
|
||||||
|
|
||||||
::warning
|
::alert{type="warning"}
|
||||||
__Warning:__
|
:::list{type="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 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 to expose a video service like Plex (if you followed [this guide](/serveex/media/plex), Plex is not exposed, so it’s fine)
|
||||||
- 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, that’s 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, that’s 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.
|
||||||
@@ -38,7 +53,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](#managing-multiple-tunnels-for-multiple-servers).
|
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).
|
||||||
|
|
||||||
### API Key
|
### API Key
|
||||||
|
|
||||||
@@ -56,14 +71,16 @@ 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 there’s 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.
|
You must register for _Cloudflare Teams_ to access the _Zero Trust_ dashboard that manages tunnels and access policies. This is a premium service, but there’s 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://one.dash.cloudflare.com/).
|
Register [via this link](https://dash.teams.cloudflare.com/).
|
||||||
|
|
||||||
## SWAG Configuration
|
## SWAG Configuration
|
||||||
::note
|
---
|
||||||
|
::alert{type="info"}
|
||||||
This guide assumes you own `mondomaine.fr` and that its DNS is correctly pointing to Cloudflare, as described above.
|
:::list{type="info"}
|
||||||
|
- 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:
|
||||||
@@ -73,71 +90,57 @@ 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.
|
||||||
|
|
||||||
::steps{level="3"}
|
### Tunnel Configuration
|
||||||
### 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`.
|
||||||
|
|
||||||
::tip{icon="" to="/serveex/files/file-browser-quantum"}
|
::alert{type="success"}
|
||||||
✨ __Tip:__ Use **File Browser Quantum** to navigate and edit files instead of using the terminal.
|
✨ __Tip:__ Use [File Browser](/serveex/files/file-browser) to navigate and edit files instead of using the terminal.
|
||||||
::
|
::
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/tunnelconfig.yml
|
sudo vi /docker/swag/config/tunnelconfig.yml
|
||||||
```
|
```
|
||||||
|
|
||||||
Paste:
|
Press `i` to enter insert mode and paste:
|
||||||
|
|
||||||
```yaml [tunnelconfig.yml]
|
```yaml
|
||||||
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 :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Esc`, then save and exit with `:x` and `Enter`.
|
||||||
|
|
||||||
### Configure Cloudflare Real IP
|
### Cloudflare Real IP Configuration
|
||||||
|
|
||||||
Now configure _Cloudflare Real IP_.
|
Now configure _Cloudflare Real IP_.
|
||||||
|
|
||||||
Open the `nginx.conf` file:
|
Open the `nginx.conf` file:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/nginx/nginx.conf
|
sudo vi /docker/swag/config/nginx/nginx.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
Add the following at the end of the `http` section:
|
Press `i` and add the following at the end of the `http` section:
|
||||||
|
|
||||||
```nginx [nginx.conf]
|
```nginx
|
||||||
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 with :kbd{value="Ctrl+O"}, then :kbd{value="Enter"}, and exit with :kbd{value="Ctrl+X"}.
|
Save and exit with `:x`.
|
||||||
|
|
||||||
### Deploy the SWAG stack
|
### Docker Compose
|
||||||
|
|
||||||
::warning
|
|
||||||
|
|
||||||
This replaces your whole SWAG service definition. If you already added a `networks:` section or `EXTRA_DOMAINS` for other apps (Dockge, TinyAuth, etc.), merge those into the block below instead of pasting over them, or you'll lose that configuration.
|
|
||||||
::
|
|
||||||
|
|
||||||
Make sure the fail2ban database file already exists, or Docker will create a directory in its place instead of mounting the file, silently breaking ban persistence:
|
|
||||||
|
|
||||||
```bash [Terminal]
|
|
||||||
sudo touch /srv/docker/swag/config/fail2ban/fail2ban.sqlite3
|
|
||||||
```
|
|
||||||
|
|
||||||
In Dockge, edit your SWAG stack with this:
|
In Dockge, edit your SWAG stack with this:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
swag:
|
swag:
|
||||||
@@ -168,16 +171,15 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 81:81
|
- 81:81
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/swag/config:/config
|
- /docker/swag/config:/config
|
||||||
- /srv/docker/swag/config/fail2ban/fail2ban.sqlite3:/dashboard/fail2ban.sqlite3:ro
|
- /docker/swag/config/fail2ban/fail2ban.sqlite3:/dashboard/fail2ban.sqlite3:ro
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
|
||||||
::tip{icon=""}
|
::alert{type="success"}
|
||||||
✨ __Tip:__ Add a Watchtower label to automate updates:
|
✨ __Tip:__ Add a Watchtower label to automate updates:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
labels:
|
labels:
|
||||||
- com.centurylinklabs.watchtower.enable=true
|
- com.centurylinklabs.watchtower.enable=true
|
||||||
```
|
```
|
||||||
@@ -185,7 +187,7 @@ labels:
|
|||||||
|
|
||||||
Fill in your `.env` file:
|
Fill in your `.env` file:
|
||||||
|
|
||||||
```properties [.env]
|
```properties
|
||||||
PUID=
|
PUID=
|
||||||
PGID=
|
PGID=
|
||||||
DOMAIN=
|
DOMAIN=
|
||||||
@@ -201,7 +203,7 @@ TUNNEL_PW=
|
|||||||
| Variable | Value | Example |
|
| Variable | Value | Example |
|
||||||
|----------------|-------------------------------------------------------------|--------------------------------|
|
|----------------|-------------------------------------------------------------|--------------------------------|
|
||||||
| `PUID` | User ID (`id username`) | `1000` |
|
| `PUID` | User ID (`id username`) | `1000` |
|
||||||
| `PGID` | Group ID (`id username`) | `1000` |
|
| `GUID` | Group ID (`id username`) | `1000` |
|
||||||
| `DOMAIN` | Your reserved domain | `mondomaine.fr` |
|
| `DOMAIN` | Your reserved domain | `mondomaine.fr` |
|
||||||
| `PLUGIN` | DNS provider (also configure `cloudflare.ini`) | `cloudflare` |
|
| `PLUGIN` | DNS provider (also configure `cloudflare.ini`) | `cloudflare` |
|
||||||
| `EMAIL` | Email for the certificate | `[email protected]` |
|
| `EMAIL` | Email for the certificate | `[email protected]` |
|
||||||
@@ -211,44 +213,36 @@ 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 logs: you 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 tunnel, no 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).
|
||||||
|
|
||||||
::tip{icon="" to="/general/networking/dns"}
|
::alert{type="success"}
|
||||||
✨ __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`.
|
✨ __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 record—e.g., for `sub.mondomaine.fr`.
|
||||||

|

|
||||||
::
|
::
|
||||||
|
|
||||||
### 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.
|
||||||
|
|
||||||
::steps{level="3"}
|
In your DNS zone, redirect subdomains to the correct tunnel.
|
||||||
### Change the tunnel name
|
|
||||||
|
|
||||||
In the second server's SWAG stack, set a different `TUNNEL_NAME` in the `.env` file, then redeploy.
|
Go to _Networks > Tunnels_ in [Cloudflare Zero Trust](https://one.dash.cloudflare.com/).
|
||||||
|
|
||||||
### Find the tunnel IDs
|
Note the tunnel IDs:
|
||||||
|
|
||||||
Go to _Networks > Tunnels_ in [Cloudflare Zero Trust](https://one.dash.cloudflare.com/) and note the tunnel IDs:
|
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Add CNAME records
|
Then in the [Cloudflare DNS dashboard](https://dash.cloudflare.com/), click your domain name.
|
||||||
|
|
||||||
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`):
|
Click `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.
|
||||||
@@ -267,4 +261,4 @@ Example:
|
|||||||
| Type | Name | Target |
|
| Type | Name | Target |
|
||||||
|---------|--------|---------------|
|
|---------|--------|---------------|
|
||||||
| `CNAME` | `sub3` | `subdomain2` |
|
| `CNAME` | `sub3` | `subdomain2` |
|
||||||
| `CNAME` | `sub4` | `subdomain2` |
|
| `CNAME` | `sub4` | `subdomain2` |
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
navigation.title: Security
|
||||||
|
icon: lucide:shield
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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, don’t 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;`.
|
||||||
|
Don’t 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.
|
||||||
|
::
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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, we’ll 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), you’ve 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 Dozzle’s 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}. Don’t forget to [create an application and a provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
|
||||||
|
::
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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`. Don’t 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 you’re 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 doesn’t know the name "speedtest-tracker". To allow access, you need to add Speedtest Tracker’s network to SWAG’s `compose.yml`.
|
||||||
|
:::
|
||||||
|
::
|
||||||
|
|
||||||
|
Go to Dockge, and edit SWAG’s compose to include Speedtest Tracker’s 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 SWAG’s 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;`.
|
||||||
|
Don’t forget to [create an application and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
|
||||||
|
::
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### 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 server’s `443` port via [NAT rules](/general/networking/nat).
|
||||||
|
::
|
||||||
|
|
||||||
|
In Dockge, edit Swag's compose file and add Beszel’s 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 Swag’s 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.
|
||||||
|
|
||||||
|
That’s 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;`. Don’t forget to [create an application and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
|
||||||
|
::
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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 router’s 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 that’s it — you’ve 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}. Don’t forget to [create an application and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
|
||||||
|
::
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
navigation.title: Monitoring
|
||||||
|
icon: lucide:chart-no-axes-column
|
||||||
+88
-98
@@ -1,69 +1,66 @@
|
|||||||
---
|
---
|
||||||
|
navigation: true
|
||||||
title: Plex
|
title: Plex
|
||||||
description: Archived guide to installing Plex Media Server with Tautulli, kept for reference — Serveex now recommends Jellyfin.
|
description: Install Plex Media Server with Tautulli on your homelab to stream movies and TV shows from anywhere on all your devices.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
---
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
# Plex
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 **Objectives:**
|
||||||
|
- Install Plex
|
||||||
::note{to="/serveex/media/jellyfin"}
|
- Install Tautulli
|
||||||
|
- Access media from outside your network
|
||||||
This is an alternative to **Jellyfin**, kept here for reference. Plex isn't fully self-hosted: it requires a Plex account even for local playback, remote access can fall back to Plex's own relay when a direct connection fails, and several features sit behind a Plex Pass paywall.
|
|
||||||
::
|
::
|
||||||
|
|
||||||
[Plex](https://www.plex.tv/) is a self-hosted video streaming platform for managing your movie or TV show library and playing them locally or remotely. Plex has apps for TV, Android, iOS, Windows, and macOS, allowing you to stream your library just like Netflix.
|
[Plex](https://www.plex.tv/fr/) is a self-hosted video streaming platform for managing your movie or TV show library and playing them locally or remotely. Plex has apps for TV, Android, iOS, Windows, and macOS, allowing you to stream your library just like Netflix.
|
||||||
|
|
||||||
With *Plex Pass*, you can also organize and play your music content similar to Spotify, the difference being that it’s your content, hosted and streamed from your server.
|
With *Plex Pass*, you can also organize and play your music content similar to Spotify, the difference being that it’s your content, hosted and streamed from your server.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
We'll also install [Tautulli](https://docs.linuxserver.io/images/docker-tautulli/), a tool that provides detailed stats about Plex. As always, we'll use linuxserver.io images where possible.
|
We'll also install [Tautulli](https://docs.linuxserver.io/images/docker-tautulli/), a tool that provides detailed stats about Plex. As always, we'll use linuxserver.io images where possible.
|
||||||
|
|
||||||
- [More info on the Plex container](https://docs.linuxserver.io/images/docker-plex)
|
- [More info on the Plex container](https://docs.linuxserver.io/images/docker-plex)
|
||||||
- [More info on the Tautulli container](https://docs.linuxserver.io/images/docker-tautulli/)
|
- [More info on the Tautulli container](https://docs.linuxserver.io/images/docker-tautulli/)
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
You’ll need to create a *Plex.tv* account. You don’t need to expose your Plex service; it will be accessible through the platform. Your Plex server will be manageable directly from your account.
|
- You’ll need to create a *Plex.tv* account. You don’t need to expose your Plex service; it will be accessible through the platform. Your Plex server will be manageable directly from your account.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
## Install Plex
|
## Install Plex
|
||||||
|
|
||||||
::file-tree
|
|
||||||
---
|
---
|
||||||
tree:
|
Folder structure:
|
||||||
/:
|
```sh
|
||||||
- srv:
|
root
|
||||||
- docker:
|
├── docker
|
||||||
- plex:
|
│ ├── plex
|
||||||
- compose.yaml
|
│ │ ├── compose.yml
|
||||||
- .env
|
│ │ ├── .env
|
||||||
- config/
|
│ │ ├── config
|
||||||
- transcode/
|
│ │ └── transcode
|
||||||
- tautulli:
|
│ └── tautulli
|
||||||
- config/
|
│ └── config
|
||||||
- media:
|
└── media
|
||||||
- tvseries/
|
├── tvseries
|
||||||
- movies/
|
├── movies
|
||||||
- library/
|
└── library
|
||||||
---
|
```
|
||||||
::
|
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Create the media folders
|
|
||||||
|
|
||||||
Create the `movies`, `tvseries`, and `library` folders in `/media`:
|
Create the `movies`, `tvseries`, and `library` folders in `/media`:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
mkdir -p /media/movies /media/library /media/tvseries
|
mkdir -p /media/movies /media/library /media/tvseries
|
||||||
```
|
```
|
||||||
|
|
||||||
### Deploy the stack
|
|
||||||
|
|
||||||
Open Dockge in your browser and click `compose`.
|
Open Dockge in your browser and click `compose`.
|
||||||
Name the stack `plex` and add the following config:
|
Name the stack `plex` and add the following config:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
linuxserver_plex:
|
linuxserver_plex:
|
||||||
@@ -72,12 +69,12 @@ services:
|
|||||||
network_mode: host
|
network_mode: host
|
||||||
environment:
|
environment:
|
||||||
- PUID=${PUID}
|
- PUID=${PUID}
|
||||||
- PGID=${PGID}
|
- PGID=${GUID}
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
- VERSION=docker
|
- VERSION=docker
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/plex/config:/config
|
- /docker/plex/config:/config
|
||||||
- /srv/docker/plex/transcode:/transcode
|
- /docker/plex/transcode:/transcode
|
||||||
- /media:/media
|
- /media:/media
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
mem_limit: 4096m
|
mem_limit: 4096m
|
||||||
@@ -90,20 +87,19 @@ services:
|
|||||||
container_name: tautulli
|
container_name: tautulli
|
||||||
environment:
|
environment:
|
||||||
- PUID=${PUID}
|
- PUID=${PUID}
|
||||||
- PGID=${PGID}
|
- PGID=${GUID}
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/tautulli/config:/config
|
- /docker/tautulli/config:/config
|
||||||
ports:
|
ports:
|
||||||
- 8181:8181
|
- 8181:8181
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
|
||||||
::tip{icon=""}
|
::alert{type="success"}
|
||||||
✨ Add the Watchtower label to each container to automate updates:
|
✨ Add the Watchtower label to each container to automate updates:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
plex:
|
plex:
|
||||||
#...
|
#...
|
||||||
@@ -117,19 +113,17 @@ services:
|
|||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
### Set your environment variables
|
Find your PUID and GUID by running:
|
||||||
|
|
||||||
Find your PUID and PGID by running:
|
```sh
|
||||||
|
|
||||||
```bash [Terminal]
|
|
||||||
id username
|
id username
|
||||||
```
|
```
|
||||||
|
|
||||||
Fill in your `.env` file with the retrieved values, for example:
|
Fill in your `.env` file with the retrieved values, for example:
|
||||||
|
|
||||||
```properties [.env]
|
```properties
|
||||||
PUID=1000
|
PUID=1000
|
||||||
PGID=1000
|
GUID=1000
|
||||||
```
|
```
|
||||||
|
|
||||||
Deploy the stack.
|
Deploy the stack.
|
||||||
@@ -137,27 +131,27 @@ Deploy the stack.
|
|||||||
The local interface is available at `http://yourserverip:32400/web/index.html`.
|
The local interface is available at `http://yourserverip:32400/web/index.html`.
|
||||||
Tautulli is accessible at `http://yourserverip:8181`.
|
Tautulli is accessible at `http://yourserverip:8181`.
|
||||||
|
|
||||||
::warning{to="https://support.plex.tv/articles/200288586-installation/#toc-2"}
|
::alert{type="warning"}
|
||||||
|
:::list{type="warning"}
|
||||||
You must be on your local network during Plex's initial setup. Otherwise, the URL will redirect to your Plex account without detecting your server. A VPN won't help. If you have no choice, **you can handle the setup remotely via SSH tunnel**.
|
- You must be on your local network during Plex's initial setup. Otherwise, the URL will redirect to your Plex account without detecting your server. A VPN won't help. If you have no choice, [you can handle the setup remotely via SSH tunnel](https://support.plex.tv/articles/200288586-installation/#toc-2).
|
||||||
::
|
:::
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
::
|
||||||
|
|
||||||
## Configure Plex
|
## Configure Plex
|
||||||
|
---
|
||||||
Plex offers a range of free movies/shows. After creating your account, I recommend disabling everything in the _Online Services_ section to keep your library clean.
|
Plex offers a range of free movies/shows. After creating your account, I recommend disabling everything in the _Online Services_ section to keep your library clean.
|
||||||
|
|
||||||
Then go to the _Remote Access_ section and manually select a port (we’ll use `1234`). It's best not to use the default port.
|
Then go to the _Remote Access_ section and manually select a port (we’ll use `1234`). It's best not to use the default port.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
- On your router, forward TCP port `1234` to port `32400` for your server’s IP using [NAT rules](/general/networking/nat).
|
- On your router, forward TCP port `1234` to port `32400` for your server’s IP using [NAT rules](/general/networking/nat).
|
||||||
- Once done, return to Plex to verify that remote access is functional.
|
- Once done, return to Plex to verify that remote access is functional.
|
||||||
|
|
||||||
::caution
|
::alert{type="danger"}
|
||||||
|
:::list{type="danger"}
|
||||||
**If it fails:** check your firewall rules and allow port `32400` on your server.
|
- **If it fails:** check your firewall rules and allow port `32400` on your server.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
- If you have PlexPass and a GPU or iGPU, enable *hardware acceleration* in the _Transcoder_ section.
|
- If you have PlexPass and a GPU or iGPU, enable *hardware acceleration* in the _Transcoder_ section.
|
||||||
@@ -168,33 +162,32 @@ And that’s it! You now have a working Plex server!
|
|||||||
|
|
||||||
Simply add your media to `/media/movies` and `/media/tvseries` on your server. You can then install the Plex app on your devices and watch your favorite content locally or remotely.
|
Simply add your media to `/media/movies` and `/media/tvseries` on your server. You can then install the Plex app on your devices and watch your favorite content locally or remotely.
|
||||||
|
|
||||||
::note{to="/general/networking/samba"}
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
If your media is stored on a network disk (e.g. NAS or external hard drive over the network), refer to the **Samba mount guide** so Plex can access it.
|
- If your media is stored on a network disk (e.g. NAS or external hard drive over the network), refer to the [Samba mount guide](/general/networking/samba) so Plex can access it.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
## Expose Tautulli with SWAG
|
## Expose Tautulli with Swag
|
||||||
|
---
|
||||||
You don’t need to expose Plex, as it's accessible via your Plex account on plex.tv.
|
You don’t need to expose Plex, as it's accessible via your Plex account on plex.tv.
|
||||||
|
|
||||||
However, you may want to expose Tautulli so you can view stats from a simple URL when you're not home.
|
However, you may want to expose Tautulli so you can view stats from a simple URL when you're not home.
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
We assume you have the subdomain `tautulli.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).
|
- We assume you have the subdomain `tautulli.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 Tautulli's network to SWAG
|
|
||||||
|
|
||||||
Go to Dockge and edit SWAG’s compose file by adding Tautulli’s network:
|
Go to Dockge and edit SWAG’s compose file by adding Tautulli’s network:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
swag:
|
swag:
|
||||||
container_name: # ...
|
container_name: # ...
|
||||||
# ...
|
# ...
|
||||||
networks: # Attach container to custom network
|
networks: # Attach container to custom network
|
||||||
# ...
|
# ...
|
||||||
- tautulli # Name of the declared network
|
- tautulli # Name of the declared network
|
||||||
|
|
||||||
@@ -207,27 +200,26 @@ networks: # Define the custom network
|
|||||||
|
|
||||||
Redeploy the stack and wait for SWAG to be fully operational.
|
Redeploy the stack and wait for SWAG to be fully operational.
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
Here we assume the Tautulli network name is `tautulli_default`. You can check the connection by visiting SWAG’s dashboard at `http://yourserverip:81`.
|
- Here we assume the Tautulli network name is `tautulli_default`. You can check the connection by visiting SWAG’s dashboard at `http://yourserverip:81`.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
### Create the subdomain.conf file
|
|
||||||
|
|
||||||
Copy and rename the file `tautulli.subdomain.conf.sample` to `tautulli.subdomain.conf`, then edit it:
|
Copy and rename the file `tautulli.subdomain.conf.sample` to `tautulli.subdomain.conf`, then edit it:
|
||||||
|
|
||||||
::tip{icon="" to="/serveex/files/file-browser-quantum"}
|
::alert{type="success"}
|
||||||
✨ **Tip:** Use **File Browser** to navigate and edit files instead of using terminal commands.
|
✨ **Tip:** Use [File Browser](/serveex/files/file-browser) to navigate and edit files instead of using terminal commands.
|
||||||
::
|
::
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo cp /srv/docker/swag/config/nginx/proxy-confs/tautulli.subdomain.conf.sample /srv/docker/swag/config/nginx/proxy-confs/tautulli.subdomain.conf
|
sudo cp /docker/swag/config/nginx/proxy-confs/tautulli.subdomain.conf.sample /docker/swag/config/nginx/proxy-confs/tautulli.subdomain.conf
|
||||||
sudo nano /srv/docker/swag/config/nginx/proxy-confs/tautulli.subdomain.conf
|
sudo vi /docker/swag/config/nginx/proxy-confs/tautulli.subdomain.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
Ensure the configuration matches the following, and edit it if needed:
|
Ensure the configuration matches the following. If needed, press `i` to edit:
|
||||||
|
|
||||||
```nginx [tautulli.subdomain.conf]
|
```nginx
|
||||||
## Version 2023/05/31
|
## Version 2023/05/31
|
||||||
# make sure that your tautulli container is named tautulli
|
# make sure that your tautulli container is named tautulli
|
||||||
# make sure that your dns has a cname set for tautulli
|
# make sure that your dns has a cname set for tautulli
|
||||||
@@ -306,20 +298,18 @@ server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
::tip{icon="" to="/serveex/advanced/authentik#protecting-an-app-via-reverse-proxy"}
|
::alert{type="success"}
|
||||||
✨ You can protect this app with Authentik by removing the `#` before `include /config/nginx/authentik-server.conf;` and `include /config/nginx/authentik-location.conf;`. Don’t forget to **create an application and provider in Authentik**.
|
✨ You can protect this app with Authentik by removing the `#` before `include /config/nginx/authentik-server.conf;` and `include /config/nginx/authentik-location.conf;`. Don’t forget to [create an application and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
|
||||||
::
|
::
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Esc` then save and quit by typing `:x`
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
Wait a few minutes, then open `http://tautulli.mydomain.com` in your browser.
|
Wait a few minutes, then open `http://tautulli.mydomain.com` in your browser.
|
||||||
|
|
||||||
::caution
|
::alert{type="danger"}
|
||||||
|
:::list{type="danger"}
|
||||||
**If it fails:** check your firewall rules.
|
- **If it fails:** check your firewall rules.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
And you're done!
|
And you're done!
|
||||||
+95
-117
@@ -1,66 +1,63 @@
|
|||||||
|
---
|
||||||
|
navigation: true
|
||||||
|
title: Qbittorrent
|
||||||
|
description: Install qBittorrent with Gluetun and ProtonVPN to download torrents securely behind a VPN on your self-hosted server.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
---
|
---
|
||||||
title: qBittorrent for Plex
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
description: Archived guide to installing qBittorrent for a Plex setup, kept for reference — see the current qBittorrent guide instead.
|
# Qbittorrent
|
||||||
---
|
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 __Goals:__
|
||||||
|
- Install and configure Qbittorrent
|
||||||
::note{to="/serveex/media/qbittorrent"}
|
- Securely connect to the BitTorrent network using Gluetun and Proton VPN
|
||||||
|
|
||||||
This is the seedbox setup paired with Plex rather than Jellyfin, kept here for reference. See **qBittorrent** for the same setup paired with Jellyfin, the recommended alternative since Plex isn't fully self-hosted (its own relay, a required account) and gates features behind a Plex Pass paywall.
|
|
||||||
::
|
::
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
To safely download your favorite media, we'll build a system using:
|
To safely download your favorite media, we'll build a system using:
|
||||||
|
|
||||||
- [qBittorrent](https://github.com/linuxserver/docker-qbittorrent) as the BitTorrent client
|
- [Qbittorrent](https://github.com/linuxserver/docker-qbittorrent) as the BitTorrent client
|
||||||
- [Proton VPN Plus](https://protonvpn.com/torrenting), a VPN to secure your traffic. You need a subscription (promos available) to access the BitTorrent protocol. You can also use another VPN as long as it supports BitTorrent.
|
- [Proton VPN Plus](https://protonvpn.com/torrenting), a VPN to secure your traffic. You need a subscription (promos available) to access the BitTorrent protocol. You can also use another VPN as long as it supports BitTorrent.
|
||||||
- [Gluetun](https://github.com/qdm12/gluetun)
|
- [Gluetun](https://github.com/qdm12/gluetun)
|
||||||
- [qBittorrent port update](https://codeberg.org/TechnoSam/qbittorrent-gluetun-port-update) to automatically update the VPN port (which changes regularly).
|
- [Qbittorrent port update](https://codeberg.org/TechnoSam/qbittorrent-gluetun-port-update) to automatically update the VPN port (which changes regularly).
|
||||||
- The [VueTorrent](https://github.com/gabe565/linuxserver-mod-vuetorrent) mod for a modern and intuitive UI.
|
- The [VueTorrent](https://github.com/gabe565/linuxserver-mod-vuetorrent) mod for a modern and intuitive UI.
|
||||||
|
|
||||||
Here’s the system we’ll set up:
|
Here’s the system we’ll set up:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
::file-tree
|
|
||||||
---
|
---
|
||||||
tree:
|
Folder structure
|
||||||
/:
|
|
||||||
- srv:
|
|
||||||
- docker:
|
|
||||||
- seedbox:
|
|
||||||
- qbittorrent:
|
|
||||||
- config/
|
|
||||||
- gluetun/
|
|
||||||
- compose.yaml
|
|
||||||
- .env
|
|
||||||
- "media # linked to Plex and qBittorrent":
|
|
||||||
- "downloads/ # generic downloads, selected in settings"
|
|
||||||
- "movies/ # used for downloading movies"
|
|
||||||
- "tvseries/ # used for downloading TV shows"
|
|
||||||
---
|
|
||||||
::
|
|
||||||
|
|
||||||
::steps{level="3"}
|
```sh
|
||||||
### Create the downloads folder
|
root
|
||||||
|
├── docker
|
||||||
|
│ └── seedbox
|
||||||
|
│ ├── qbittorrent
|
||||||
|
│ │ └── config
|
||||||
|
│ ├── gluetun
|
||||||
|
│ ├── compose.yaml
|
||||||
|
│ └── .env
|
||||||
|
│
|
||||||
|
└── media #linked to Plex and Qbittorrent
|
||||||
|
├── downloads #generic downloads, selected in settings
|
||||||
|
├── movies #used for downloading movies
|
||||||
|
└── tvseries #used for downloading TV shows
|
||||||
|
```
|
||||||
|
|
||||||
If not already done, create the `downloads` folder under `/media`:
|
If not already done, create the `downloads` folder under `/media`:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
mkdir -p /media/downloads
|
mkdir -P /media/downloads
|
||||||
```
|
```
|
||||||
|
|
||||||
### Deploy the stack
|
|
||||||
|
|
||||||
Open Dockge, click on `compose`, and name the stack `seedbox`. Paste the following config:
|
Open Dockge, click on `compose`, and name the stack `seedbox`. Paste the following config:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
qbit:
|
qbit:
|
||||||
image: ghcr.io/linuxserver/qbittorrent:libtorrentv1
|
image: ghcr.io/linuxserver/qbittorrent:libtorrentv1
|
||||||
@@ -72,13 +69,13 @@ services:
|
|||||||
- DOCKER_MODS=ghcr.io/gabe565/linuxserver-mod-vuetorrent|ghcr.io/t-anc/gsp-qbittorent-gluetun-sync-port-mod:main
|
- DOCKER_MODS=ghcr.io/gabe565/linuxserver-mod-vuetorrent|ghcr.io/t-anc/gsp-qbittorent-gluetun-sync-port-mod:main
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
- PUID=${PUID}
|
- PUID=${PUID}
|
||||||
- PGID=${PGID}
|
- PGID=${GUID}
|
||||||
- WEBUI_PORT=${UI_PORT}
|
- WEBUI_PORT=${UI_PORT}
|
||||||
- GSP_GTN_API_KEY=${GSP_KEY}
|
- GSP_GTN_API_KEY=${GSP_KEY}
|
||||||
- GSP_QBT_USERNAME=${ID}
|
- GSP_QBT_USERNAME=${ID}
|
||||||
- GSP_QBT_PASSWORD=${PW}
|
- GSP_QBT_PASSWORD=${PW}
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/seedbox/qbittorrent/config:/config
|
- /docker/seedbox/qbittorrent/config:/config
|
||||||
- /media:/media
|
- /media:/media
|
||||||
depends_on:
|
depends_on:
|
||||||
- gluetun
|
- gluetun
|
||||||
@@ -89,11 +86,11 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
mem_limit: 4g
|
mem_limit: 4g
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/gluetun/config.toml:/gluetun/auth/config.toml:ro
|
- /docker/gluetun/config.toml:/gluetun/auth/config.toml:ro
|
||||||
devices:
|
devices:
|
||||||
- /dev/net/tun:/dev/net/tun
|
- /dev/net/tun:/dev/net/tun
|
||||||
ports:
|
ports:
|
||||||
- ${UI_PORT}:${UI_PORT} # Port de la web-ui
|
- ${UI_PORT}:5695 # Port de la web-ui
|
||||||
- 8000:8000 # Port de controle de Gluetun
|
- 8000:8000 # Port de controle de Gluetun
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
@@ -108,11 +105,10 @@ services:
|
|||||||
- PORT_FORWARD_ONLY=on
|
- PORT_FORWARD_ONLY=on
|
||||||
```
|
```
|
||||||
|
|
||||||
::tip{icon=""}
|
::alert{type="success"}
|
||||||
✨ __Tip:__ Add the Watchtower label in each container to automate updates
|
✨ __Tip:__ Add the Watchtower label in each container to automate updates
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
qbittorrent:
|
qbittorrent:
|
||||||
#...
|
#...
|
||||||
@@ -125,9 +121,7 @@ services:
|
|||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
### Configure the port-forwarding sync
|
Before editing the `.env` in Dockge, let's configure the download port update. Proton and most VPNs rotate the forwarding port, which must be communicated to Qbittorrent.
|
||||||
|
|
||||||
Before editing the `.env` in Dockge, let's configure the download port update. Proton and most VPNs rotate the forwarding port, which must be communicated to qBittorrent.
|
|
||||||
|
|
||||||
We’ve added the mod `ghcr.io/t-anc/gsp-qbittorent-gluetun-sync-port-mod` to the container.
|
We’ve added the mod `ghcr.io/t-anc/gsp-qbittorent-gluetun-sync-port-mod` to the container.
|
||||||
|
|
||||||
@@ -135,25 +129,25 @@ We now need to allow the mod to fetch info from Gluetun, which only allows encry
|
|||||||
|
|
||||||
Open a terminal to generate the authentication key:
|
Open a terminal to generate the authentication key:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo docker run --rm qmcgaw/gluetun genkey
|
sudo docker run --rm qmcgaw/gluetun genkey
|
||||||
```
|
```
|
||||||
|
|
||||||
Note the key, then create the `/srv/docker/gluetun` folder:
|
Note the key, then create the `/docker/gluetun` folder:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo mkdir /srv/docker/gluetun
|
sudo mkdir /docker/gluetun
|
||||||
```
|
```
|
||||||
|
|
||||||
Create the `config.toml` file:
|
Create the `config.toml` file:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/gluetun/config.toml
|
sudo vi /docker/gluetun/config.toml
|
||||||
```
|
```
|
||||||
|
|
||||||
Enter:
|
Press `i` to edit and enter:
|
||||||
|
|
||||||
```toml [config.toml]
|
```toml
|
||||||
[[roles]]
|
[[roles]]
|
||||||
name = "t-anc/GSP-Qbittorent-Gluetun-sync-port-mod"
|
name = "t-anc/GSP-Qbittorent-Gluetun-sync-port-mod"
|
||||||
routes = ["GET /v1/portforward"]
|
routes = ["GET /v1/portforward"]
|
||||||
@@ -161,15 +155,13 @@ auth = "apikey"
|
|||||||
apikey = "your_key_here" # key you just generated
|
apikey = "your_key_here" # key you just generated
|
||||||
```
|
```
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Esc` then type `:x` to save and exit.
|
||||||
|
|
||||||
### Set your environment variables
|
|
||||||
|
|
||||||
In Dockge, fill in the variables in `.env`:
|
In Dockge, fill in the variables in `.env`:
|
||||||
|
|
||||||
```properties [.env]
|
```properties
|
||||||
PUID=
|
PUID=
|
||||||
PGID=
|
GUID=
|
||||||
UI_PORT=
|
UI_PORT=
|
||||||
PR_KEY=
|
PR_KEY=
|
||||||
GSP_KEY= # the key you generated and entered in config.toml
|
GSP_KEY= # the key you generated and entered in config.toml
|
||||||
@@ -182,67 +174,57 @@ Detailed info:
|
|||||||
| Variable | Description | Example |
|
| Variable | Description | Example |
|
||||||
|------------|-------------|---------|
|
|------------|-------------|---------|
|
||||||
| `PUID` | User ID (`id yourusername`) | `1000` |
|
| `PUID` | User ID (`id yourusername`) | `1000` |
|
||||||
| `PGID` | Group ID (`id yourusername`) | `1000` |
|
| `GUID` | Group ID (`id yourusername`) | `1000` |
|
||||||
| `UI_PORT` | Port for accessing the web UI | `5695` |
|
| `UI_PORT` | Port for accessing the web UI | `5695` |
|
||||||
| `PR_KEY` | Private key from Proton | `buKsjNHLyzKMM1qYnzOy4s7SHfly` |
|
| `PR_KEY` | Private key from Proton | `buKsjNHLyzKMM1qYnzOy4s7SHfly` |
|
||||||
| `GSP_KEY` | Key you generated for port update | `MnBa47MeVmk7xiv` |
|
| `GSP_KEY` | Key you generated for port update | `MnBa47MeVmk7xiv` |
|
||||||
| `ID` | qBittorrent UI login username | `user` |
|
| `ID` | Qbittorrent UI login username | `user` |
|
||||||
| `PW` | qBittorrent UI password | `password` |
|
| `PW` | Qbittorrent UI password | `password` |
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
## Deployment
|
## Deployment
|
||||||
|
---
|
||||||
::steps{level="3"}
|
|
||||||
### Deploy the container
|
|
||||||
|
|
||||||
Once done, deploy the container.
|
Once done, deploy the container.
|
||||||
|
|
||||||
::warning
|
::alert{type="warning"}
|
||||||
|
:::list{type="warning"}
|
||||||
**Startup logs will show a temporary password for `admin` user**
|
- **Startup logs will show a temporary password for `admin` user**
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
### Log in and secure your account
|
Login at `http://server-ip:5695` (or the port you set).
|
||||||
|
|
||||||
Login at `http://yourserverip:5695` (or the port you set).
|
::alert{type="danger"}
|
||||||
|
:::list{type="danger"}
|
||||||
::caution
|
- __If login fails:__ check your firewall rules.
|
||||||
|
:::
|
||||||
__If login fails:__ check your firewall rules.
|
|
||||||
::
|
::
|
||||||
|
|
||||||
Change your username and password in the "webui" settings.
|
Change your username and password in the "webui" settings.
|
||||||
|
|
||||||
### Done!
|
You're done! In Qbittorrent settings, under "Downloads", set `/media/downloads` as the default folder.
|
||||||
::
|
|
||||||
|
|
||||||
You're done! In qBittorrent settings, under "Downloads", set `/media/downloads` as the default folder.
|
|
||||||
|
|
||||||
When adding a download, remember to select the proper directory so Plex can sync correctly (`/media/movies` or `/media/tvseries`). You can also automate this with categories and folders.
|
When adding a download, remember to select the proper directory so Plex can sync correctly (`/media/movies` or `/media/tvseries`). You can also automate this with categories and folders.
|
||||||
|
|
||||||
## Exposing the Web UI
|
## Exposing the Web UI
|
||||||
|
---
|
||||||
|
|
||||||
::warning
|
::alert{type="warning"}
|
||||||
|
:::list{type="warning"}
|
||||||
qBittorrent does not support multi-factor authentication. Exposing it to the internet may put your system at risk. Only do this if you use MFA via [Authentik](/serveex/advanced/authentik/). Otherwise, don’t expose it with SWAG. Use a VPN like [WireGuard](/serveex/core/wireguard) instead.
|
- Qbittorrent does not support multi-factor authentication. Exposing it to the internet may put your system at risk. Only do this if you use MFA via [Authentik](/serveex/security/authentik/). Otherwise, don’t expose it with SWAG—use a VPN like [Wireguard](/serveex/security/wireguard) instead.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
To start downloads from outside your home, without a VPN, you can expose the qBittorrent web UI.
|
To start downloads from outside your home, without a VPN, you can expose the Qbittorrent web UI.
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
We assume you have the subdomain `seedbox.mydomain.com` with a `CNAME` pointing to `mydomain.com` in [DNS zone](/general/networking/dns). And that port `443` on your router is forwarded to your server in [NAT rules](/general/networking/nat), unless you’re using Cloudflare Zero Trust.
|
- We assume you have the subdomain `seedbox.mydomain.com` with a `CNAME` pointing to `mydomain.com` in [DNS zone](/general/networking/dns). And that port `443` on your router is forwarded to your server in [NAT rules](/general/networking/nat), unless you’re using Cloudflare Zero Trust.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Add the seedbox network to SWAG
|
|
||||||
|
|
||||||
In Dockge, edit the SWAG compose file and add Gluetun’s network:
|
In Dockge, edit the SWAG compose file and add Gluetun’s network:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
swag:
|
swag:
|
||||||
container_name: # ...
|
container_name: # ...
|
||||||
@@ -260,26 +242,25 @@ networks:
|
|||||||
|
|
||||||
Click "Deploy" and wait for SWAG to fully initialize.
|
Click "Deploy" and wait for SWAG to fully initialize.
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
We assume the network name is `seedbox_default`. You can confirm by checking the SWAG dashboard at http://yourserverip:81.
|
- We assume the network name is `seedbox_default`. You can confirm by checking the SWAG dashboard at http://server-ip:81.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
### Create the subdomain.conf file
|
|
||||||
|
|
||||||
Now create/edit `seedbox.subdomain.conf`.
|
Now create/edit `seedbox.subdomain.conf`.
|
||||||
|
|
||||||
::tip{icon="" to="/serveex/files/file-browser-quantum"}
|
::alert{type="success"}
|
||||||
✨ __Terminal-free tip:__ use **File Browser** to edit files instead of using the terminal.
|
✨ __Terminal-free tip:__ use [File Browser](/serveex/files/file-browser) to edit files instead of using the terminal.
|
||||||
::
|
::
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/nginx/proxy-confs/seedbox.subdomain.conf
|
sudo vi /docker/swag/config/nginx/proxy-confs/seedbox.subdomain.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
Paste the following config (check the port):
|
Press `i` and paste the following config (check the port):
|
||||||
|
|
||||||
```nginx [seedbox.subdomain.conf]
|
```nginx
|
||||||
## Version 2023/12/19
|
## Version 2023/12/19
|
||||||
|
|
||||||
server {
|
server {
|
||||||
@@ -322,7 +303,7 @@ server {
|
|||||||
include /config/nginx/proxy.conf;
|
include /config/nginx/proxy.conf;
|
||||||
include /config/nginx/resolver.conf;
|
include /config/nginx/resolver.conf;
|
||||||
set $upstream_app gluetun;
|
set $upstream_app gluetun;
|
||||||
set $upstream_port 5695;
|
set $upstream_port 5555;
|
||||||
set $upstream_proto http;
|
set $upstream_proto http;
|
||||||
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
|
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
|
||||||
|
|
||||||
@@ -330,17 +311,14 @@ server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
::tip{icon="" to="/serveex/advanced/authentik#protecting-an-app-via-reverse-proxy"}
|
::alert{type="success"}
|
||||||
✨ You can secure this app with Authentik by uncommenting the `authentik-server.conf` and `authentik-location.conf` lines. Don’t forget to **create an app and provider in Authentik**.
|
✨ You can secure this app with Authentik by uncommenting the `authentik-server.conf` and `authentik-location.conf` lines. Don’t forget to [create an app and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
|
||||||
::
|
::
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Esc`, type `:x` to save and quit.
|
||||||
|
|
||||||
### Done!
|
Wait a few minutes, then go to `https://seedbox.mydomain.com`—you should land on the Qbittorrent interface.
|
||||||
::
|
|
||||||
|
|
||||||
Wait a few minutes, then go to `https://seedbox.mydomain.com`. You should land on the qBittorrent interface.
|
|
||||||
|
|
||||||
And that’s it! You now have a ready-to-use media center.
|
And that’s it! You now have a ready-to-use media center.
|
||||||
|
|
||||||

|

|
||||||
+125
-148
@@ -1,18 +1,19 @@
|
|||||||
---
|
---
|
||||||
title: Servarr for Plex
|
navigation: true
|
||||||
description: Archived guide to the Servarr stack for a Plex setup, kept for reference — see the current Servarr guide instead.
|
title: Automation
|
||||||
|
description: Automate media downloads with the Servarr stack — Radarr, Sonarr, Bazarr, Prowlarr, and Overseerr for movies and TV shows.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
---
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
# Servarr
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 __Goals:__
|
||||||
|
- Automate movie and TV show downloads using Radarr, Sonarr, Bazarr, Prowlarr, and Overseerr.
|
||||||
::note{to="/serveex/media/servarr"}
|
|
||||||
|
|
||||||
This is the Servarr stack wired up for Plex rather than Jellyfin, kept here for reference. See **Automation** for the same stack paired with Jellyfin, the recommended alternative since Plex isn't fully self-hosted (its own relay, a required account) and gates features behind a Plex Pass paywall.
|
|
||||||
::
|
::
|
||||||
|
|
||||||
[Servarr](https://wiki.servarr.com/) is a suite of applications developed to automate the downloading, updating, and management of media. Here, we'll focus on movies and TV shows with the goal of:
|
[Servarr](https://wiki.servarr.com/) is a suite of applications developed to automate the downloading, updating, and management of media. Here, we'll focus on movies and TV shows with the goal of:
|
||||||
|
|
||||||
- Selecting a movie from a catalog through a web interface.
|
- Selecting a movie from a catalog through a web interface.
|
||||||
- Sitting back and enjoying it on Plex a few minutes later.
|
- Sitting back and enjoying it on Plex a few minutes later.
|
||||||
|
|
||||||
@@ -23,49 +24,48 @@ Simple.
|
|||||||
We’ll start by deploying the stack and then proceed to configure each app and understand how they work.
|
We’ll start by deploying the stack and then proceed to configure each app and understand how they work.
|
||||||
|
|
||||||
## Install the Apps
|
## Install the Apps
|
||||||
|
|
||||||
::file-tree
|
|
||||||
---
|
|
||||||
tree:
|
|
||||||
/:
|
|
||||||
- srv:
|
|
||||||
- docker:
|
|
||||||
- plex:
|
|
||||||
- compose.yaml
|
|
||||||
- config/
|
|
||||||
- transcode/
|
|
||||||
- tautulli:
|
|
||||||
- config/
|
|
||||||
- sonarr:
|
|
||||||
- config/
|
|
||||||
- radarr:
|
|
||||||
- config/
|
|
||||||
- bazarr:
|
|
||||||
- config/
|
|
||||||
- prowlarr:
|
|
||||||
- config/
|
|
||||||
- overseerr:
|
|
||||||
- config/
|
|
||||||
- media:
|
|
||||||
- downloads/
|
|
||||||
- tvseries/
|
|
||||||
- movies/
|
|
||||||
- library/
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Docker Compose
|
||||||
|
|
||||||
|
Folder structure:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
root
|
||||||
|
├── docker
|
||||||
|
│ ├── plex
|
||||||
|
│ │ ├── compose.yml
|
||||||
|
│ │ ├── config
|
||||||
|
│ │ └── transcode
|
||||||
|
│ ├── tautulli
|
||||||
|
│ │ └── config
|
||||||
|
│ ├── sonarr
|
||||||
|
│ │ └── config
|
||||||
|
│ ├── radarr
|
||||||
|
│ │ └── config
|
||||||
|
│ ├── bazarr
|
||||||
|
│ │ └── config
|
||||||
|
│ ├── prowlarr
|
||||||
|
│ │ └── config
|
||||||
|
│ └── overseerr
|
||||||
|
│ └── config
|
||||||
|
└── media
|
||||||
|
├── downloads
|
||||||
|
├── tvseries
|
||||||
|
├── movies
|
||||||
|
└── library
|
||||||
|
```
|
||||||
|
|
||||||
|
::alert{type="warning"}
|
||||||
|
:::list{type="warning"}
|
||||||
|
- __Warning:__ Make sure to follow this file structure carefully, especially the `media` folder. This folder must be mounted **exactly the same way** in both the _Qbittorrent_ compose file (`/your/path/media:/media`) and the _arr_ applications.
|
||||||
|
If not, the _arr_ apps may not recognize the path provided by Qbittorrent and will fail to create _hardlinks_.
|
||||||
|
Without hardlinks, the _arr_ apps will copy the files instead—**doubling the space used** on your storage.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
::warning
|
|
||||||
|
|
||||||
__Warning:__ Make sure to follow this file structure carefully, especially the `media` folder. This folder must be mounted **exactly the same way** in both the _Qbittorrent_ compose file (`/your/path/media:/media`) and the _arr_ applications.
|
|
||||||
If not, the _arr_ apps may not recognize the path provided by qBittorrent and will fail to create _hardlinks_.
|
|
||||||
Without hardlinks, the _arr_ apps will copy the files instead, **doubling the space used** on your storage.
|
|
||||||
::
|
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Deploy the stack
|
|
||||||
|
|
||||||
Open Docker and your `plex` stack. Modify the compose file as follows:
|
Open Docker and your `plex` stack. Modify the compose file as follows:
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
linuxserver_plex:
|
linuxserver_plex:
|
||||||
@@ -79,8 +79,8 @@ services:
|
|||||||
- VERSION=docker
|
- VERSION=docker
|
||||||
- PLEX_CLAIM= #optional
|
- PLEX_CLAIM= #optional
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/plex/config:/config
|
- /docker/plex/config:/config
|
||||||
- /srv/docker/plex/transcode:/transcode #optional
|
- /docker/plex/transcode:/transcode #optional
|
||||||
- ${MEDIA_PATH}:/media
|
- ${MEDIA_PATH}:/media
|
||||||
labels:
|
labels:
|
||||||
- com.centurylinklabs.watchtower.enable=true
|
- com.centurylinklabs.watchtower.enable=true
|
||||||
@@ -96,7 +96,7 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/tautulli/config:/config
|
- /docker/tautulli/config:/config
|
||||||
ports:
|
ports:
|
||||||
- 8181:8181
|
- 8181:8181
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -109,7 +109,7 @@ services:
|
|||||||
- PGID=${PGID}
|
- PGID=${PGID}
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/sonarr/config:/config
|
- /docker/sonarr/config:/config
|
||||||
- ${MEDIA_PATH}:/media
|
- ${MEDIA_PATH}:/media
|
||||||
ports:
|
ports:
|
||||||
- 8989:8989
|
- 8989:8989
|
||||||
@@ -123,7 +123,7 @@ services:
|
|||||||
- PGID=${PGID}
|
- PGID=${PGID}
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/radarr/config:/config
|
- /docker/radarr/config:/config
|
||||||
- ${MEDIA_PATH}:/media
|
- ${MEDIA_PATH}:/media
|
||||||
ports:
|
ports:
|
||||||
- 7878:7878
|
- 7878:7878
|
||||||
@@ -137,7 +137,7 @@ services:
|
|||||||
- PGID=${PGID}
|
- PGID=${PGID}
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/prowlarr/data:/config
|
- /docker/prowlarr/data:/config
|
||||||
ports:
|
ports:
|
||||||
- 9696:9696
|
- 9696:9696
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -153,7 +153,7 @@ services:
|
|||||||
- PGID=${PGID}
|
- PGID=${PGID}
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/overseerr/config:/config
|
- /docker/overseerr/config:/config
|
||||||
ports:
|
ports:
|
||||||
- 5055:5055
|
- 5055:5055
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -167,17 +167,16 @@ services:
|
|||||||
- PGID=1000
|
- PGID=1000
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/bazarr/config:/config
|
- /docker/bazarr/config:/config
|
||||||
- ${MEDIA_PATH}:/media
|
- ${MEDIA_PATH}:/media
|
||||||
ports:
|
ports:
|
||||||
- 6767:6767
|
- 6767:6767
|
||||||
```
|
```
|
||||||
|
|
||||||
::tip{icon=""}
|
::alert{type="success"}
|
||||||
✨ Add the Watchtower label to each container to automate updates
|
✨ Add the Watchtower label to each container to automate updates
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
plex:
|
plex:
|
||||||
#...
|
#...
|
||||||
@@ -191,67 +190,64 @@ services:
|
|||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
### Set your environment variables
|
|
||||||
|
|
||||||
Set your `.env` file with the variables below:
|
Set your `.env` file with the variables below:
|
||||||
|
|
||||||
```properties [.env]
|
```properties
|
||||||
PUID=
|
PUID=
|
||||||
PGID=
|
GUID=
|
||||||
MEDIA_PATH=
|
MEDIA_PATH=
|
||||||
```
|
```
|
||||||
|
|
||||||
| Variable | Description | Example |
|
| Variable | Description | Example |
|
||||||
|----------------|-------------------------------------------------------------------------------------------------|-------------|
|
|----------------|-------------------------------------------------------------------------------------------------|-------------|
|
||||||
| `PUID` | Set using your user info (check with `id yourusername`) | `1000` |
|
| `PUID` | Set using your user info (check with `id yourusername`) | `1000` |
|
||||||
| `PGID` | Same as above | `1000` |
|
| `GUID` | Same as above | `1000` |
|
||||||
| `MEDIA_PATH` | Path to your media folder, here: `/media`. It must match the one used by qBittorrent. | `/media` |
|
| `MEDIA_PATH` | Path to your media folder, here: `/media`. It must match the one used by Qbittorrent. | `/media` |
|
||||||
|
|
||||||
Deploy the stack.
|
Deploy the stack.
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
### Configure Radarr
|
### Configure Radarr
|
||||||
|
---
|
||||||
|
|
||||||
Radarr queries your torrent sources and lets you define the type of releases to prioritize. It can also upgrade your movies if a better version is available.
|
Radarr queries your torrent sources and lets you define the type of releases to prioritize. It can also upgrade your movies if a better version is available.
|
||||||
|
|
||||||
Once deployed, visit `http://yourserverip:7878`.
|
Once deployed, visit `http://yourserverip:7878`.
|
||||||
|
|
||||||
::caution
|
::alert{type="danger"}
|
||||||
|
:::list{type="danger"}
|
||||||
__If it fails:__ check your firewall rules.
|
- __If it fails:__ check your firewall rules.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
Create an account and choose *forms login*.
|
Create an account and choose *forms login*.
|
||||||
|
|
||||||
::steps{level="4"}
|
##### Add a *root folder*
|
||||||
#### Add a *root folder*
|
|
||||||
|
|
||||||
- Go to *Settings > Media Management*.
|
- Go to *Settings > Media Management*.
|
||||||
- Add a root folder and select `/media/movies`.
|
- Add a root folder and select `/media/movies`.
|
||||||
|
|
||||||
::warning
|
::alert{type="warning"}
|
||||||
|
:::list{type="warning"}
|
||||||
__Warning:__ If you already have movies in `movies` from qBittorrent, do not let Radarr add them. Radarr might modify them, which could stop seeding in qBittorrent.
|
- __Warning:__ If you already have movies in `movies` from Qbittorrent, do not let Radarr add them. Radarr might modify them, which could stop seeding in Qbittorrent.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
#### Configure Profiles
|
##### Configure Profiles
|
||||||
|
|
||||||
Go to *Settings > Profiles*. These are your default quality profiles. When you make a request, you're selecting one of these. For example, configure the “any” profile by unchecking everything except what is shown in the image and ordering them accordingly. This makes Radarr search for 4K REMUX first, then go down the list if unavailable.
|
Go to *Settings > Profiles*. These are your default quality profiles. When you make a request, you're selecting one of these. For example, configure the “any” profile by unchecking everything except what is shown in the image and ordering them accordingly. This makes Radarr search for 4K REMUX first, then go down the list if unavailable.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### Add qBittorrent
|
##### Add Qbittorrent
|
||||||
|
|
||||||
In *Settings > Download Clients*, add qBittorrent.
|
In *Settings > Download Clients*, add Qbittorrent.
|
||||||
|
|
||||||
- Use your server IP as *Host* and port `5695` if following this guide.
|
- Use your server IP as *Host* and port `5695` if following this guide.
|
||||||
- Provide your qBittorrent *Username* and *Password*.
|
- Provide your Qbittorrent *Username* and *Password*.
|
||||||
- Click *Test*.
|
- Click *Test*.
|
||||||
- If successful, click *Save*.
|
- If successful, click *Save*.
|
||||||
|
|
||||||
#### Connect to Plex
|
##### Connect to Plex
|
||||||
|
|
||||||
Go to *Settings > Connect*, add a new connection and choose *Plex Media Server*.
|
Go to *Settings > Connect*, add a new connection and choose *Plex Media Server*.
|
||||||
|
|
||||||
@@ -260,42 +256,42 @@ Go to *Settings > Connect*, add a new connection and choose *Plex Media Server*.
|
|||||||
- Click the blue "authenticate with Plex.tv" button and log into Plex.
|
- Click the blue "authenticate with Plex.tv" button and log into Plex.
|
||||||
- Press *Test*, then *Save* if successful.
|
- Press *Test*, then *Save* if successful.
|
||||||
|
|
||||||
#### Get API Key for Prowlarr and Overseerr
|
##### Get API Key for Prowlarr and Overseerr
|
||||||
|
|
||||||
- Go to *Settings > General* and copy your *API Key* for later use.
|
- Go to *Settings > General* and copy your *API Key* for later use.
|
||||||
|
|
||||||
#### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
### Configure Sonarr
|
### Configure Sonarr
|
||||||
|
---
|
||||||
|
|
||||||
Sonarr queries torrent sources and defines what kind of TV series releases to prioritize. It also upgrades series when better versions are available.
|
Sonarr queries torrent sources and defines what kind of TV series releases to prioritize. It also upgrades series when better versions are available.
|
||||||
|
|
||||||
- Visit `http://yourserverip:8989`.
|
- Visit `http://yourserverip:8989`.
|
||||||
- Follow the same steps as for Radarr, but use `/media/tvseries` as the root folder.
|
- Follow the same steps as for Radarr, but use `/media/tvseries` as the root folder.
|
||||||
|
|
||||||
::caution
|
::alert{type="danger"}
|
||||||
|
:::list{type="danger"}
|
||||||
__If it fails:__ check your firewall rules.
|
- __If it fails:__ check your firewall rules.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
### Configure Prowlarr
|
### Configure Prowlarr
|
||||||
|
---
|
||||||
|
|
||||||
Prowlarr acts as a proxy to manage your torrent indexers and link them to Radarr and Sonarr.
|
Prowlarr acts as a proxy to manage your torrent indexers and link them to Radarr and Sonarr.
|
||||||
|
|
||||||
Go to `http://yourserverip:9696` and create an account, using *forms login*.
|
Go to `http://yourserverip:9696` and create an account, using *forms login*.
|
||||||
|
|
||||||
::caution
|
::alert{type="danger"}
|
||||||
|
:::list{type="danger"}
|
||||||
__If it fails:__ check your firewall rules.
|
- __If it fails:__ check your firewall rules.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
::steps{level="4"}
|
##### Add an Indexer
|
||||||
#### Add an Indexer
|
|
||||||
|
|
||||||
- Go to the *Indexers* section and add your torrent indexer.
|
- Go to the *Indexers* section and add your torrent indexer.
|
||||||
|
|
||||||
#### Add Radarr and Sonarr
|
##### Add Radarr and Sonarr
|
||||||
|
|
||||||
In *Settings > Apps*, add Radarr and Sonarr with the following details:
|
In *Settings > Apps*, add Radarr and Sonarr with the following details:
|
||||||
|
|
||||||
@@ -304,22 +300,20 @@ In *Settings > Apps*, add Radarr and Sonarr with the following details:
|
|||||||
- API Key: use the one copied from Radarr and Sonarr.
|
- API Key: use the one copied from Radarr and Sonarr.
|
||||||
- Click *Test*, then *Save* if all goes well.
|
- Click *Test*, then *Save* if all goes well.
|
||||||
|
|
||||||
#### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
### Configuring Bazarr
|
### Configuring Bazarr
|
||||||
|
---
|
||||||
Bazarr is an app that automatically searches for the correct subtitles in your preferred languages for all the movies and TV shows added by Radarr and Sonarr.
|
Bazarr is an app that automatically searches for the correct subtitles in your preferred languages for all the movies and TV shows added by Radarr and Sonarr.
|
||||||
|
|
||||||
Go to `http://yourserverip:6767`.
|
Go to `http://yourserverip:6767`.
|
||||||
|
|
||||||
::caution
|
::alert{type="danger"}
|
||||||
|
:::list{type="danger"}
|
||||||
__If it fails:__ check your firewall rules.
|
- __If it fails:__ check your firewall rules.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
Go to *Settings > General* and create a username and password using *forms login*.
|
Go to *Settings > General* and create a username and password using *forms login*.
|
||||||
|
|
||||||
::steps{level="4"}
|
|
||||||
#### Add a Language Profile
|
#### Add a Language Profile
|
||||||
|
|
||||||
- In *Settings > Languages*, click the pink *Add new profile* button and name it.
|
- In *Settings > Languages*, click the pink *Add new profile* button and name it.
|
||||||
@@ -327,7 +321,6 @@ Go to *Settings > General* and create a username and password using *forms login
|
|||||||
- Save and exit.
|
- Save and exit.
|
||||||
- At the bottom of the screen under *Default Language For Newly Added Show*, check both boxes and select the profile you just created.
|
- At the bottom of the screen under *Default Language For Newly Added Show*, check both boxes and select the profile you just created.
|
||||||

|

|
||||||
|
|
||||||
- Save using the button at the top of the screen.
|
- Save using the button at the top of the screen.
|
||||||
|
|
||||||
#### Add Subtitle Providers
|
#### Add Subtitle Providers
|
||||||
@@ -349,10 +342,8 @@ Go to *Settings > General* and create a username and password using *forms login
|
|||||||
|
|
||||||
Repeat the same steps for Radarr.
|
Repeat the same steps for Radarr.
|
||||||
|
|
||||||
#### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
### Configuring Overseerr
|
### Configuring Overseerr
|
||||||
|
---
|
||||||
|
|
||||||
[Overseerr](https://overseerr.dev/) is an app that lets you browse a movie catalog and send requests to Sonarr and Radarr. Just browse movies or series, click *Request*, and the media will automatically be downloaded according to your Radarr or Sonarr settings. If the title hasn’t been released yet, it will be downloaded automatically when available. This way, episodes of a series appear in Plex without any manual intervention.
|
[Overseerr](https://overseerr.dev/) is an app that lets you browse a movie catalog and send requests to Sonarr and Radarr. Just browse movies or series, click *Request*, and the media will automatically be downloaded according to your Radarr or Sonarr settings. If the title hasn’t been released yet, it will be downloaded automatically when available. This way, episodes of a series appear in Plex without any manual intervention.
|
||||||
|
|
||||||
@@ -360,16 +351,15 @@ Repeat the same steps for Radarr.
|
|||||||
|
|
||||||
Go to `http://yourserverip:5055` and log in with your Plex account.
|
Go to `http://yourserverip:5055` and log in with your Plex account.
|
||||||
|
|
||||||
::caution
|
::alert{type="danger"}
|
||||||
|
:::list{type="danger"}
|
||||||
__If it fails:__ check your firewall rules.
|
- __If it fails:__ check your firewall rules.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
::steps{level="4"}
|
#### Add Radarr and Sonarr
|
||||||
#### Add Radarr
|
|
||||||
|
|
||||||
When prompted, add a Radarr server:
|
When prompted, add a Radarr server:
|
||||||
|
|
||||||
- Check *Default server*.
|
- Check *Default server*.
|
||||||
- __Server name:__ Radarr
|
- __Server name:__ Radarr
|
||||||
- __Hostname or IP address:__ `radarr` or your server's IP
|
- __Hostname or IP address:__ `radarr` or your server's IP
|
||||||
@@ -378,17 +368,13 @@ When prompted, add a Radarr server:
|
|||||||
- Click *Test* at the bottom.
|
- Click *Test* at the bottom.
|
||||||
|
|
||||||
If the test succeeds, continue filling in the fields:
|
If the test succeeds, continue filling in the fields:
|
||||||
|
|
||||||
- __Quality Profile:__ the one you configured (e.g., `any`)
|
- __Quality Profile:__ the one you configured (e.g., `any`)
|
||||||
- __Root Folder:__ the Plex folder. In our examples: `/media/movies`
|
- __Root Folder:__ the Plex folder. In our examples: `/media/movies`
|
||||||
- __Minimum Availability:__ `Announced`. This allows requesting unreleased content and downloads it upon release.
|
- __Minimum Availability:__ `Announced`. This allows requesting unreleased content and downloads it upon release.
|
||||||
- Check all 3 boxes at the bottom.
|
- Check all 3 boxes at the bottom.
|
||||||
- Save and continue.
|
- Save and continue.
|
||||||
|
|
||||||
#### Add Sonarr
|
|
||||||
|
|
||||||
Now do the same for Sonarr:
|
Now do the same for Sonarr:
|
||||||
|
|
||||||
- Check *Default server*.
|
- Check *Default server*.
|
||||||
- __Server name:__ Sonarr
|
- __Server name:__ Sonarr
|
||||||
- __Hostname or IP address:__ `sonarr` or your server's IP
|
- __Hostname or IP address:__ `sonarr` or your server's IP
|
||||||
@@ -397,39 +383,33 @@ Now do the same for Sonarr:
|
|||||||
- Click *Test* at the bottom.
|
- Click *Test* at the bottom.
|
||||||
|
|
||||||
If the test succeeds, continue filling in the fields:
|
If the test succeeds, continue filling in the fields:
|
||||||
|
|
||||||
- __Quality Profile:__ the one you configured (e.g., `any`)
|
- __Quality Profile:__ the one you configured (e.g., `any`)
|
||||||
- __Root Folder:__ the Plex folder. In our examples: `/media/tvseries`
|
- __Root Folder:__ the Plex folder. In our examples: `/media/tvseries`
|
||||||
- __Language Profile:__ `Deprecated`
|
- __Language Profile:__ `Deprecated`
|
||||||
- Check all 4 boxes at the bottom.
|
- Check all 4 boxes at the bottom.
|
||||||
- Save and continue.
|
- Save and continue.
|
||||||
|
|
||||||
#### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
And that’s it! Just request a movie or series, then check in qBittorrent or Radarr/Sonarr. Within a few minutes, your media will be available on Plex!
|
And that’s it! Just request a movie or series, then check in qBittorrent or Radarr/Sonarr. Within a few minutes, your media will be available on Plex!
|
||||||
|
|
||||||
## Exposing Overseerr with SWAG
|
## Exposing Overseerr with SWAG
|
||||||
|
---
|
||||||
|
|
||||||
It can be useful to expose Overseerr if you want to send requests from outside your network without a VPN, or if you've shared your Plex library with others and want them to have Overseerr access.
|
It can be useful to expose Overseerr if you want to send requests from outside your network without a VPN, or if you've shared your Plex library with others and want them to have Overseerr access.
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
We assume you have the subdomain `films.mydomain.com` with a `CNAME` pointing to `mydomain.com` in your [DNS zone](/general/networking/dns). And that [unless you’re using Cloudflare Zero Trust](/serveex/security/cloudflare), port `443` on your router is forwarded to port `443` on your server via [NAT rules](/general/networking/nat).
|
- We assume you have the subdomain `films.mydomain.com` with a `CNAME` pointing to `films.fr` in your [DNS zone](/general/networking/dns). And that [unless you’re using Cloudflare Zero Trust](/serveex/security/cloudflare), port `443` on your router is forwarded to port `443` on your server via [NAT rules](/general/networking/nat).
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Add Overseerr's network to SWAG
|
|
||||||
|
|
||||||
Go to Dockge, edit the SWAG compose file, and add the Overseerr network, which is the same as Plex (since it’s in the Plex stack):
|
Go to Dockge, edit the SWAG compose file, and add the Overseerr network, which is the same as Plex (since it’s in the Plex stack):
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
swag:
|
swag:
|
||||||
container_name: # ...
|
container_name: # ...
|
||||||
# ...
|
# ...
|
||||||
networks: # Connects the container to a custom network
|
networks: # Connects the container to a custom network
|
||||||
# ...
|
# ...
|
||||||
- plex # Name of the network declared in the stack
|
- plex # Name of the network declared in the stack
|
||||||
|
|
||||||
@@ -442,26 +422,25 @@ networks: # Defines the custom network
|
|||||||
|
|
||||||
Restart the stack by clicking “Deploy” and wait until SWAG is fully operational.
|
Restart the stack by clicking “Deploy” and wait until SWAG is fully operational.
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
Here we assume the Tautulli network is named `plex_default`. You can verify the connection works by visiting the SWAG dashboard at `http://yourserverip:81`.
|
- Here we assume the Tautulli network is named `plex_default`. You can verify the connection works by visiting the SWAG dashboard at `http://yourserverip:81`.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
### Create the subdomain.conf file
|
|
||||||
|
|
||||||
Create and edit the file `films.subdomain.conf`:
|
Create and edit the file `films.subdomain.conf`:
|
||||||
|
|
||||||
::tip{icon="" to="/serveex/files/file-browser-quantum"}
|
::alert{type="success"}
|
||||||
✨ __Tip:__ you can use **File Browser** to browse and edit files instead of using terminal commands.
|
✨ __Tip:__ you can use [File Browser](/serveex/files/file-browser) to browse and edit files instead of using terminal commands.
|
||||||
::
|
::
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/nginx/proxy-confs/films.subdomain.conf
|
sudo vi /docker/swag/config/nginx/proxy-confs/films.subdomain.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
Paste the following:
|
Enter insert mode by pressing `i`:
|
||||||
|
|
||||||
```nginx [films.subdomain.conf]
|
```nginx
|
||||||
## Version 2024/07/16
|
## Version 2024/07/16
|
||||||
# make sure that your overseerr container is named overseerr
|
# make sure that your overseerr container is named overseerr
|
||||||
# make sure that your dns has a cname set for overseerr
|
# make sure that your dns has a cname set for overseerr
|
||||||
@@ -520,16 +499,14 @@ server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Escape`, then type `:x` and press `Enter` to save and exit.
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
Wait a few minutes, then visit `http://films.mydomain.com` in your browser.
|
Wait a few minutes, then visit `http://films.mydomain.com` in your browser.
|
||||||
|
|
||||||
::caution
|
::alert{type="danger"}
|
||||||
|
:::list{type="danger"}
|
||||||
__If it fails:__ check your firewall rules.
|
- __If it fails:__ check your firewall rules.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
And there you go, Overseerr is now publicly accessible!
|
And there you go, Overseerr is now publicly accessible!
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
navigation.title: Media & Seedbox
|
||||||
|
icon: lucide:list-video
|
||||||
@@ -1,79 +1,76 @@
|
|||||||
---
|
---
|
||||||
|
navigation: true
|
||||||
title: Immich
|
title: Immich
|
||||||
description: Install Immich, a self-hosted alternative to Google Photos and iCloud with face recognition, geolocation, and multi-device sync.
|
description: Install Immich, a self-hosted alternative to Google Photos and iCloud with face recognition, geolocation, and multi-device sync.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
---
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
# Immich
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 __Goals:__ Install [Immich](https://immich.app/docs/overview/introduction) to manage your photos across all your devices.
|
||||||
|
::
|
||||||
|
|
||||||
[Immich](https://immich.app/docs/overview/introduction) is a self-hosted photo and video management solution that replaces cloud services like Google Photos or iCloud. It offers powerful features like face recognition and geolocation.
|
[Immich](https://immich.app/docs/overview/introduction) is a self-hosted photo and video management solution that replaces cloud services like Google Photos or iCloud. It offers powerful features like face recognition and geolocation.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
::file-tree
|
|
||||||
---
|
---
|
||||||
tree:
|
Folder structure
|
||||||
/:
|
|
||||||
- srv:
|
|
||||||
- docker:
|
|
||||||
- immich:
|
|
||||||
- library/
|
|
||||||
- compose.yaml
|
|
||||||
- .env
|
|
||||||
---
|
|
||||||
::
|
|
||||||
|
|
||||||
::steps{level="3"}
|
```sh
|
||||||
### Deploy the stack
|
root
|
||||||
|
└── docker
|
||||||
|
└── immich
|
||||||
|
├── library
|
||||||
|
├── compose.yaml
|
||||||
|
└── .env
|
||||||
|
```
|
||||||
|
|
||||||
Open Dockge, click on `compose`, name the stack `immich`, then copy and paste the latest `docker-compose.yml` [published here](https://github.com/immich-app/immich/blob/main/docker/docker-compose.yml).
|
Open Dockge, click on `compose`, name the stack `immich`, then copy and paste the latest `docker-compose.yml` [published here](https://github.com/immich-app/immich/blob/main/docker/docker-compose.yml).
|
||||||
|
|
||||||
::warning
|
::alert{type="warning"}
|
||||||
|
:::list{type="warning"}
|
||||||
__Warning__: Do not add the Watchtower label to the Immich stack. Immich evolves rapidly, and automatic updates may break your installation.
|
- __Warning__: Do not add the Watchtower label to the Immich stack. Immich evolves rapidly, and automatic updates may break your installation.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
### Set your environment variables
|
|
||||||
|
|
||||||
Configure the `.env` file by copying the latest version [from here](https://github.com/immich-app/immich/blob/main/docker/example.env) and follow the comments in the file.
|
Configure the `.env` file by copying the latest version [from here](https://github.com/immich-app/immich/blob/main/docker/example.env) and follow the comments in the file.
|
||||||
|
|
||||||
::note{to="/general/networking/samba/"}
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
If you're using a NAS or a network-shared drive via **Samba** to store your data, replace the value of `UPLOAD_LOCATION`{lang=properties} with the path to your shared folder.
|
- If you're using a NAS or a network-shared drive via [Samba](/general/networking/samba/) to store your data, replace the value of `UPLOAD_LOCATION`{lang=properties} with the path to your shared folder.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
::tip{icon=""}
|
::alert{type="success"}
|
||||||
✨ __Tip:__ If your CPU/iGPU/GPU supports it, Immich can use hardware acceleration for video playback and image recognition. This can triple performance. Learn more about [Transcoding](https://immich.app/docs/features/hardware-transcoding/) and [Machine Learning](https://immich.app/docs/features/ml-hardware-acceleration).
|
✨ __Tip:__ If your CPU/iGPU/GPU supports it, Immich can use hardware acceleration for video playback and image recognition. This can triple performance. Learn more about [Transcoding](https://immich.app/docs/features/hardware-transcoding/) and [Machine Learning](https://immich.app/docs/features/ml-hardware-acceleration).
|
||||||
::
|
::
|
||||||
|
|
||||||
Deploy the container. You can connect and follow the setup instructions at `http://yourserverip:2283`.
|
Deploy the container.
|
||||||
|
|
||||||
### Done!
|
You're done! You can connect and follow the setup instructions at `http://yourserverip:2283`.
|
||||||
::
|
|
||||||
|
|
||||||
## Exposing Immich with SWAG
|
## Exposing Immich with SWAG
|
||||||
|
---
|
||||||
The main benefit of this setup is being able to access Immich remotely on all your devices. We'll expose Immich using SWAG.
|
The main benefit of this setup is being able to access Immich remotely on all your devices. We'll expose Immich using SWAG.
|
||||||
|
|
||||||
::note{icon=""}
|
::alert{type="info"}
|
||||||
📋 __Before you begin:__
|
📋 __Before you begin:__
|
||||||
<br/><br/>
|
<br/><br/>
|
||||||
We assume that you have a subdomain `immich.yourdomain.com` with a `CNAME` pointing to `yourdomain.com` in your [DNS zone](/general/networking/dns). Also, unless you're using [Cloudflare Zero Trust](/serveex/security/cloudflare), make sure port `443` on your router is forwarded to port `443` on your server via [NAT rules](/general/networking/nat).
|
We assume that you have a subdomain `immich.yourdomain.com` with a `CNAME` pointing to `yourdomain.com` in your [DNS zone](/general/networking/dns). Also, unless you're using [Cloudflare Zero Trust](/serveex/security/cloudflare), make sure port `443` on your router is forwarded to port `443` on your server via [NAT rules](/general/networking/nat).
|
||||||
::
|
::
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Add Immich's network to SWAG
|
|
||||||
|
|
||||||
In Dockge, open the SWAG stack and edit the compose file to add Immich's network:
|
In Dockge, open the SWAG stack and edit the compose file to add Immich's network:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
swag:
|
swag:
|
||||||
container_name: # ...
|
container_name: # ...
|
||||||
# ...
|
# ...
|
||||||
networks: # Connects the container to the custom network
|
networks: # Connects the container to the custom network
|
||||||
# ...
|
# ...
|
||||||
- immich # Network name defined in the stack
|
- immich # Network name defined in the stack
|
||||||
|
|
||||||
@@ -84,28 +81,28 @@ networks: # Defines the custom network
|
|||||||
external: true # Indicates it's an external network
|
external: true # Indicates it's an external network
|
||||||
```
|
```
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
We're assuming Immich's network is named `immich_default`. You can check connectivity by visiting the SWAG dashboard at http://yourserverip:81.
|
- We're assuming Immich's network is named `immich_default`. You can check connectivity by visiting the SWAG dashboard at http://yourserverip:81.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
Restart the stack by clicking "deploy" and wait for SWAG to fully initialize.
|
Restart the stack by clicking "deploy" and wait for SWAG to fully initialize.
|
||||||
|
|
||||||
### Create the subdomain.conf file
|
|
||||||
|
|
||||||
In the SWAG folders, create a file named `immich.subdomain.conf`.
|
In the SWAG folders, create a file named `immich.subdomain.conf`.
|
||||||
|
|
||||||
::tip{icon="" to="/serveex/files/file-browser-quantum"}
|
::alert{type="success"}
|
||||||
|
:::list{type="success"}
|
||||||
✨ __Tip:__ You can use **File Browser Quantum** to navigate and edit your files instead of using terminal commands.
|
- __Tip:__ You can use [File Browser](/serveex/files/file-browser) to navigate and edit your files instead of using terminal commands.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/nginx/proxy-confs/immich.subdomain.conf
|
sudo vi /docker/swag/config/nginx/proxy-confs/immich.subdomain.conf
|
||||||
```
|
```
|
||||||
Then paste the following configuration:
|
Press `i` to enter insert mode, then paste the following configuration:
|
||||||
|
|
||||||
```nginx [immich.subdomain.conf]
|
```nginx
|
||||||
## Version 2023/12/19
|
## Version 2023/12/19
|
||||||
|
|
||||||
server {
|
server {
|
||||||
@@ -164,46 +161,10 @@ server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Esc`, type `:x`, then hit `Enter` to save and exit.
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
That's it! Immich is now accessible from the internet. Don’t forget to install the [iOS](https://apps.apple.com/us/app/immich/id1613945652) / [Android](https://play.google.com/store/apps/details?id=app.alextran.immich) apps to sync your devices.
|
That's it! Immich is now accessible from the internet. Don’t forget to install the [iOS](https://apps.apple.com/us/app/immich/id1613945652) / [Android](https://play.google.com/store/apps/details?id=app.alextran.immich) apps to sync your devices.
|
||||||
|
|
||||||
## Protecting Immich with Pocket ID
|
::alert{type="success"}
|
||||||
Immich supports OIDC natively, so you can require a Pocket ID login before letting anyone into your photos, instead of (or alongside) the app's own accounts.
|
✨ __Tip:__ You can protect this app with Authentik natively by [following these instructions](https://docs.goauthentik.io/integrations/services/immich/).
|
||||||
|
::
|
||||||
::steps{level="3"}
|
|
||||||
### Register Immich as an OIDC client
|
|
||||||
|
|
||||||
[Register an OIDC client in Pocket ID](/serveex/security/pocket-id#registering-an-oidc-client) named `immich`, with these three callback URLs:
|
|
||||||
|
|
||||||
```text
|
|
||||||
https://immich.yourdomain.com/auth/login
|
|
||||||
https://immich.yourdomain.com/user-settings
|
|
||||||
app.immich:///oauth-callback
|
|
||||||
```
|
|
||||||
|
|
||||||
The third URL is what lets the mobile app log in too.
|
|
||||||
|
|
||||||
### Enable OAuth in Immich
|
|
||||||
|
|
||||||
In Immich, go to _Administration > Settings > Authentication Settings > OAuth_, enable it, and fill in:
|
|
||||||
|
|
||||||
| Field | Value |
|
|
||||||
|-------|-------|
|
|
||||||
| Issuer URL | Pocket ID's OIDC discovery URL |
|
|
||||||
| Client ID | The client ID copied from Pocket ID |
|
|
||||||
| Client Secret | The client secret copied from Pocket ID |
|
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
::tip{icon="" to="/serveex/advanced/authentik"}
|
|
||||||
✨ You can use **Authentik** instead of Pocket ID:
|
|
||||||
|
|
||||||
1. In Authentik, create an application and an OAuth2/OpenID Connect provider named `immich`, with the same three redirect URIs as above (type `Strict`).
|
|
||||||
2. Note the provider's __Client ID__, __Client Secret__, and __Slug__.
|
|
||||||
3. In Immich's OAuth settings, set the Issuer URL to `https://authentik.yourdomain.com/application/o/<slug>/`, then fill in the Client ID and Client Secret.
|
|
||||||
::
|
|
||||||
@@ -0,0 +1,199 @@
|
|||||||
|
---
|
||||||
|
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. It’s a great alternative to services like Google Drive, iCloud, or OneDrive.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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 you’re 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. We’ll 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 you’re 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 Nextcloud’s 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—don’t change it!
|
||||||
|
1 => 'nextcloud.yourdomain.com', # Add your domain here
|
||||||
|
),
|
||||||
|
```
|
||||||
|
|
||||||
|
Press `Esc`, then save and exit by typing `:x` and hitting Enter.
|
||||||
|
|
||||||
|
In Swag’s 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.
|
||||||
|
|
||||||
|
That’s it—you’ve exposed Nextcloud! Don’t 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/).
|
||||||
|
::
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
navigation.title: Cloud Drive & Photos
|
||||||
|
icon: lucide:cloud-upload
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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`. That’s it—your File Browser web UI is up and running!
|
||||||
|
|
||||||
|
::alert{type="danger"}
|
||||||
|
:::list{type="danger"}
|
||||||
|
- __If it doesn’t 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 you’re 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, we’ll 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 Browser’s 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`.
|
||||||
|
|
||||||
|
That’s 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}. Don’t forget to [create an application and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
|
||||||
|
::
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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`.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
3. Enregistrez et créez une application `pingvin` comme suit.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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 d’utilisateur 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.
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
navigation.title: File & share
|
||||||
|
icon: lucide:folder-tree
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Installation
|
||||||
|
---
|
||||||
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
|
- For this setup, we’ll 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, we’ll expose code-server via Swag.
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
|
- __Preliminary:__ We assume you’ve 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 you’ve 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-server’s 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 it’s 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`.
|
||||||
|
|
||||||
|
That’s 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;`. Don’t forget to [create an application and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
|
||||||
|
::
|
||||||
+54
-68
@@ -1,39 +1,37 @@
|
|||||||
---
|
---
|
||||||
|
navigation: true
|
||||||
title: Gitea
|
title: Gitea
|
||||||
description: Archived guide to installing Gitea, kept for reference — Serveex now recommends Forgejo.
|
description: Install Gitea, a lightweight self-hosted Git service to manage your code repositories privately on your own server.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
---
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
# Gitea
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 __Goals:__
|
||||||
|
- Install Gitea
|
||||||
::note{to="/serveex/development/forgejo"}
|
- Expose Gitea using Swag
|
||||||
|
|
||||||
This is an alternative to **Forgejo**, the community-run fork created after Gitea's governance moved to a for-profit company, a concern for a project people expect to stay FOSS.
|
|
||||||
::
|
::
|
||||||
|
|
||||||
[Gitea](https://about.gitea.com/) is a self-hosted DevOps platform that allows you to manage repositories much like GitHub, but on your own infrastructure.
|
[Gitea](https://about.gitea.com/) is a self-hosted DevOps platform that allows you to manage repositories much like GitHub, but on your own infrastructure.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
::file-tree
|
|
||||||
---
|
---
|
||||||
tree:
|
Folder structure
|
||||||
/:
|
|
||||||
- srv:
|
|
||||||
- docker:
|
|
||||||
- gitea:
|
|
||||||
- data/
|
|
||||||
---
|
|
||||||
::
|
|
||||||
|
|
||||||
::steps{level="3"}
|
```sh
|
||||||
### Deploy the stack
|
root
|
||||||
|
└── docker
|
||||||
|
└── gitea
|
||||||
|
└── data
|
||||||
|
```
|
||||||
|
|
||||||
Open Dockge, click on `compose`, name the stack `gitea`, and paste the following content:
|
Open Dockge, click on `compose`, name the stack `gitea`, and paste the following content:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
---
|
||||||
networks:
|
networks:
|
||||||
gitea:
|
gitea:
|
||||||
@@ -56,45 +54,39 @@ services:
|
|||||||
- 222:22
|
- 222:22
|
||||||
```
|
```
|
||||||
|
|
||||||
### Set your environment variables
|
|
||||||
|
|
||||||
Fill out the `.env` file with the required information, for example:
|
Fill out the `.env` file with the required information, for example:
|
||||||
|
|
||||||
```properties [.env]
|
```properties
|
||||||
UID=1000
|
UID=1000
|
||||||
GID=1000
|
GID=1000
|
||||||
```
|
```
|
||||||
|
|
||||||
Deploy the container and go to `http://yourserverip:3333`. Your Gitea instance is now up and running!
|
Deploy the container and go to `http://yourserverip:3333`. Your Gitea instance is now up and running!
|
||||||
|
|
||||||
### Done!
|
::alert{type="danger"}
|
||||||
|
:::list{type="danger"}
|
||||||
|
- __If it fails:__ check your firewall rules.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
::caution
|
## Exposing Gitea with Swag
|
||||||
|
---
|
||||||
|
The benefit of this setup is being able to access it remotely from any of your devices. To do so, we’ll expose Gitea through Swag.
|
||||||
|
|
||||||
__If it fails:__ check your firewall rules.
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
|
- __Prerequisite:__ We assume you have created a subdomain such as `gitea.yourdomain.com` in your [DNS zone](/general/networking/dns) with `CNAME` pointing to `yourdomain.com`, and [unless you're using Cloudflare Zero Trust](/serveex/security/cloudflare), you have already forwarded port `443` from your router to your server’s port `443` in the [NAT rules](/general/networking/nat).
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
## Exposing Gitea with SWAG
|
|
||||||
The benefit of this setup is being able to access it remotely from any of your devices. To do so, we’ll expose Gitea through SWAG.
|
|
||||||
|
|
||||||
::note
|
|
||||||
|
|
||||||
__Prerequisite:__ We assume you have created a subdomain such as `gitea.yourdomain.com` in your [DNS zone](/general/networking/dns) with `CNAME` pointing to `yourdomain.com`, and [unless you're using Cloudflare Zero Trust](/serveex/security/cloudflare), you have already forwarded port `443` from your router to your server’s port `443` in the [NAT rules](/general/networking/nat).
|
|
||||||
::
|
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Add Gitea's network to SWAG
|
|
||||||
|
|
||||||
In Dockge, go to the SWAG stack and edit the compose file by adding Gitea's network:
|
In Dockge, go to the SWAG stack and edit the compose file by adding Gitea's network:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
swag:
|
swag:
|
||||||
container_name: # ...
|
container_name: # ...
|
||||||
# ...
|
# ...
|
||||||
networks: # Connect the container to the custom network
|
networks: # Connect the container to the custom network
|
||||||
# ...
|
# ...
|
||||||
- gitea # Name of the declared network
|
- gitea # Name of the declared network
|
||||||
|
|
||||||
@@ -105,27 +97,26 @@ networks: # Define the custom network
|
|||||||
external: true # Indicates it's an external network
|
external: true # Indicates it's an external network
|
||||||
```
|
```
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
We assume the Gitea network name is `gitea_default`. You can verify connectivity by visiting the SWAG dashboard at http://yourserverip:81.
|
- We assume the Gitea network name is `gitea_default`. You can verify connectivity by visiting the SWAG dashboard at http://yourserverip:81.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
Redeploy the stack by clicking "Deploy" and wait until SWAG is fully operational.
|
Redeploy the stack by clicking "Deploy" and wait until SWAG is fully operational.
|
||||||
|
|
||||||
### Create the subdomain.conf file
|
Inside the Swag folders, create the file `gitea.subdomain.conf`.
|
||||||
|
|
||||||
Inside the SWAG folders, create the file `gitea.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.
|
||||||
::tip{icon="" to="/serveex/files/file-browser-quantum"}
|
|
||||||
✨ __Tip:__ You can use **File Browser** to navigate and edit your files instead of using terminal commands.
|
|
||||||
::
|
::
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/nginx/proxy-confs/gitea.subdomain.conf
|
sudo vi /docker/swag/config/nginx/proxy-confs/gitea.subdomain.conf
|
||||||
```
|
```
|
||||||
Paste the configuration below:
|
Press `i` to enter edit mode and paste the configuration below:
|
||||||
|
|
||||||
```nginx [gitea.subdomain.conf]
|
```nginx
|
||||||
## Version 2023/12/19
|
## Version 2023/12/19
|
||||||
|
|
||||||
server {
|
server {
|
||||||
@@ -182,32 +173,27 @@ server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Esc`, then save and exit by typing `:x` and hitting `Enter`.
|
||||||
|
|
||||||
### Configure Gitea's domain
|
|
||||||
|
|
||||||
Now open the `app.ini` file from the container's file system:
|
Now open the `app.ini` file from the container's file system:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/gitea/data/gitea/conf/app.ini
|
sudo vi /docker/gitea/data/gitea/conf/app.ini
|
||||||
```
|
```
|
||||||
|
|
||||||
Then modify the server section with your domain information:
|
Press `i` to edit, then modify the server section with your domain information:
|
||||||
|
|
||||||
```properties [app.ini]
|
```properties
|
||||||
[server]
|
[server]
|
||||||
DOMAIN = gitea.yourdomain.com
|
DOMAIN = gitea.yourdomain.com
|
||||||
SSH_DOMAIN = gitea.yourdomain.com
|
SSH_DOMAIN = gitea.yourdomain.com
|
||||||
ROOT_URL = https://gitea.yourdomain.com/
|
ROOT_URL = https://gitea.yourdomain.com/
|
||||||
```
|
```
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit, then restart the container.
|
Press `Esc`, save and exit with `:x`, then restart the container.
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
And that’s it! Gitea is now exposed to the web.
|
And that’s it! Gitea is now exposed to the web.
|
||||||
|
|
||||||
::tip{icon=""}
|
::alert{type="success"}
|
||||||
✨ __Tip:__ You can protect this app natively via OIDC with [Pocket ID](/serveex/security/pocket-id) (register it as an OIDC client), or with Authentik by [following these instructions](https://integrations.goauthentik.io/development/gitea/).
|
✨ __Tip:__ You can natively protect this app with Authentik by [following these instructions](https://docs.goauthentik.io/integrations/services/gitea/).
|
||||||
::
|
::
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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`. That’s 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 you’ve created a subdomain like `tools.yourdomain.com` in your [DNS zone](/general/networking/dns) with `CNAME` set to `yourdomain.com`. Also, unless you’re using [Cloudflare Zero Trust](/serveex/security/cloudflare), make sure you’ve 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 that’s 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;`. Don’t forget to [create an application and a provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
|
||||||
|
::
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
navigation.title: Developpement
|
||||||
|
icon: lucide:code-xml
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
---
|
||||||
|
navigation: true
|
||||||
|
title: Adguard Home
|
||||||
|
description: Install AdGuard Home for network-wide ad and tracker blocking with DNS-over-HTTPS, client management, and custom filtering rules.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
# Adguard Home
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
🎯 __Goals:__
|
||||||
|
- Install and deploy Adguard
|
||||||
|
- Expose Adguard
|
||||||
|
- Secure DNS queries with SSL/TLS
|
||||||
|
- Configure client devices
|
||||||
|
::
|
||||||
|
|
||||||
|
[AdGuard Home](https://github.com/AdguardTeam/AdGuardHome) is a DNS server that blocks ads and tracking at the system level. Once configured, it will protect ALL your home devices without the need for any client-side software.
|
||||||
|
|
||||||
|
It works as a DNS server that redirects tracking domains to a “black hole,” preventing your devices from connecting to them.
|
||||||
|
|
||||||
|
In practice, once it's in place, all you need to do is set your devices to use Adguard as their DNS server.
|
||||||
|
|
||||||
|
**Quick reminder of how DNS works:**
|
||||||
|
|
||||||
|
When you visit a site or use an app, it makes requests to various domains to load content—ads in particular. Your device doesn’t know the IP addresses of these domains, so it contacts a _Domain Name Server_ (DNS), which returns the current IP address.
|
||||||
|
|
||||||
|
By default, your device uses your ISP's DNS server, which is usually configured in your router or, for mobile devices, at the carrier’s CGNAT level. You can change this in your browser settings, your device’s system settings, or even directly in your router, depending on your ISP.
|
||||||
|
|
||||||
|
Adguard will act as a middleman between your device and the upstream DNS servers. If you configure your devices to use Adguard:
|
||||||
|
|
||||||
|
- If the domain is not in a blocklist, Adguard queries the upstream DNS servers and returns the correct IP to your device.
|
||||||
|
- If the domain *is* in a blocklist, Adguard will block the request and return nothing, so the associated content won’t load.
|
||||||
|
|
||||||
|
This is how ads and malicious domains are blocked—Adguard blocks only the bad domains, allowing the rest of the page to load normally.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Installation
|
||||||
|
---
|
||||||
|
Folder structure:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
root
|
||||||
|
└── docker
|
||||||
|
└── adguard
|
||||||
|
├── confdir
|
||||||
|
├── workdir
|
||||||
|
├── compose.yaml
|
||||||
|
└── .env
|
||||||
|
```
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
|
- We will also mount the `/docker/swag/config/etc/letsencrypt` folder to access Swag's SSL certificate.
|
||||||
|
:::
|
||||||
|
::
|
||||||
|
|
||||||
|
Open Dockge and click `compose`
|
||||||
|
|
||||||
|
Name the stack `adguardhome` and paste the configuration below:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
---
|
||||||
|
services:
|
||||||
|
adguardhome:
|
||||||
|
container_name: adguard
|
||||||
|
image: adguard/adguardhome
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 53:53/udp
|
||||||
|
- 8080:80/tcp
|
||||||
|
- 4443:443/tcp
|
||||||
|
- 853:853/tcp
|
||||||
|
- 3000:3000/tcp
|
||||||
|
volumes:
|
||||||
|
- /docker/adguardhome/confdir:/opt/adguardhome/conf
|
||||||
|
- /docker/adguardhome/workdir:/opt/adguardhome/work
|
||||||
|
- /docker/swag/config/etc/letsencrypt:/swag-ssl:ro
|
||||||
|
```
|
||||||
|
|
||||||
|
::alert{type="success"}
|
||||||
|
✨ __Tip:__ Add the watchtower label to each container to automate updates
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
adguardhome:
|
||||||
|
# ...
|
||||||
|
labels:
|
||||||
|
- com.centurylinklabs.watchtower.enable=true
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
Deploy the stack.
|
||||||
|
|
||||||
|
Go to `http://yourserverip:3000` and follow the setup instructions.
|
||||||
|
|
||||||
|
That’s it! Adguard is deployed.
|
||||||
|
|
||||||
|
|
||||||
|
## Exposing AdGuard with SWAG
|
||||||
|
---
|
||||||
|
To make AdGuard usable from outside your home network, you need to expose it.
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
|
- __Prerequisites:__ We assume you've created a subdomain like `adguard.mydomain.com` in your [DNS zone](/general/networking/dns) with a `CNAME` pointing to `mydomain.com`, and that you’ve already forwarded port `443` from your router to port `443` on your server in your [NAT rules](/general/networking/nat). Also forward port `53` and port `853` to your server. These ports are used to route DNS requests.
|
||||||
|
:::
|
||||||
|
::
|
||||||
|
|
||||||
|
::alert{type="warning"}
|
||||||
|
:::list{type="warning"}
|
||||||
|
- Do not use Cloudflare tunnels to expose AdGuard, and make sure any proxying is disabled.
|
||||||
|
:::
|
||||||
|
::
|
||||||
|
|
||||||
|
In Dockge, go to the SWAG stack and edit the compose file to add the AdGuard network:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
swag:
|
||||||
|
container_name: # ...
|
||||||
|
# ...
|
||||||
|
networks: # Connect the container to the custom network
|
||||||
|
# ...
|
||||||
|
- adguard # Name of the network declared in the stack
|
||||||
|
|
||||||
|
networks: # Define the custom network
|
||||||
|
# ...
|
||||||
|
adguard: # Name of the network declared in the stack
|
||||||
|
name: adguard_default # Actual name of the external network
|
||||||
|
external: true # Specifies that this is an external network
|
||||||
|
```
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
|
- We assume here that the AdGuard network is named `adguard_default`. You can verify the connection is working by visiting the SWAG dashboard at http://yourserverip:81.
|
||||||
|
:::
|
||||||
|
::
|
||||||
|
|
||||||
|
Restart the stack by clicking "Deploy" and wait for SWAG to be fully operational.
|
||||||
|
|
||||||
|
Create and open the file `adguard.subdomain.conf`
|
||||||
|
|
||||||
|
::alert{type="success"}
|
||||||
|
✨ __Tip for terminal haters:__
|
||||||
|
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/adguard.subdomain.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
Edit the file by pressing `i` and then pasting the configuration below:
|
||||||
|
|
||||||
|
```nginx
|
||||||
|
## Version 2023/05/31
|
||||||
|
# make sure that your adguard container is named adguard
|
||||||
|
# make sure that your dns has a cname set for adguard
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 443 ssl;
|
||||||
|
listen [::]:443 ssl;
|
||||||
|
|
||||||
|
server_name adguard.*;
|
||||||
|
|
||||||
|
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 adguard;
|
||||||
|
set $upstream_port 3000;
|
||||||
|
set $upstream_proto http;
|
||||||
|
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location /control {
|
||||||
|
include /config/nginx/proxy.conf;
|
||||||
|
include /config/nginx/resolver.conf;
|
||||||
|
set $upstream_app adguard;
|
||||||
|
set $upstream_port 3000;
|
||||||
|
set $upstream_proto http;
|
||||||
|
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
location /dns-query {
|
||||||
|
# to properly use this please set `allow_unencrypted_doh: true` and `force_https: false` in adguard
|
||||||
|
# see https://github.com/AdguardTeam/AdGuardHome/wiki/Configuration#configuration-file
|
||||||
|
include /config/nginx/proxy.conf;
|
||||||
|
include /config/nginx/resolver.conf;
|
||||||
|
set $upstream_app adguard;
|
||||||
|
set $upstream_port 3000;
|
||||||
|
set $upstream_proto http;
|
||||||
|
proxy_pass $upstream_proto://$upstream_app:$upstream_port;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
::alert{type="success"}
|
||||||
|
✨ __Tip:__
|
||||||
|
<br/><br/>
|
||||||
|
You can protect this app with Authentik by opening `adguard.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}. Don’t forget to [create an application and a provider in Authentik](/serveex/security/authentik/#protéger-une-app-par-reverse-proxy). You’ll need to exclude the URL `https://adguard.mydomain.com/dns-query` from authentication:
|
||||||
|
|
||||||
|
- Edit the AdGuard provider
|
||||||
|
- Under *Advanced Protocol Settings > Authenticated Paths*, enter `^/dns-query`
|
||||||
|
::
|
||||||
|
|
||||||
|
Press `Esc`, then save and exit by typing `:x`
|
||||||
|
|
||||||
|
And that's it! AdGuard is now exposed!
|
||||||
|
|
||||||
|
## Configure SSL/TLS Encryption
|
||||||
|
---
|
||||||
|
Encryption is essential if you want to keep your queries to AdGuard private. Encrypting your queries ensures that no one—not even your ISP—can see your history. It also ensures that only your server can respond to you.
|
||||||
|
|
||||||
|
To configure encryption:
|
||||||
|
|
||||||
|
- Go to _Settings_ then _Encryption_.
|
||||||
|
- Set the options as follows:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
- Below, in the _Certificates_ section, check _Use file path for certificate_
|
||||||
|
- In the input field, enter `/swag-ssl/live/mydomain.com/fullchain.pem`, replacing `mydomain.com` with your actual domain.
|
||||||
|
- For _Private Key_, check _Use file path for private key_
|
||||||
|
- In the input field, enter `/swag-ssl/live/mydomain.com/privkey.pem`, replacing `mydomain.com` accordingly.
|
||||||
|
- Save
|
||||||
|
|
||||||
|
Done! Your future DNS queries are now protected!
|
||||||
|
|
||||||
|
## Configure Devices
|
||||||
|
---
|
||||||
|
You have several options (which you can combine) to configure your devices.
|
||||||
|
### Secure the Local Network
|
||||||
|
You can secure your local network with AdGuard by configuring your router to direct all DNS queries by default to AdGuard instead of your ISP’s DNS. Note: your router must allow DNS changes (Orange routers do not).
|
||||||
|
|
||||||
|
This option is usually in the _DHCP_ settings. Make sure to add a fallback DNS server such as:
|
||||||
|
|
||||||
|
- Cloudflare: `1.1.1.1`
|
||||||
|
- Google: `8.8.8.8`
|
||||||
|
|
||||||
|
Without this, if your server goes down, your devices will lose internet access.
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
|
- Some devices may have a separate DNS configured and may not use the router’s DNS.
|
||||||
|
:::
|
||||||
|
::
|
||||||
|
|
||||||
|
### Force a Browser to Use AdGuard
|
||||||
|
|
||||||
|
In your browser, you can configure a DNS to force it to use AdGuard Home.
|
||||||
|
In the settings, specify the address `https://adguard.mydomain.com/dns-query`
|
||||||
|
|
||||||
|
### Configure AdGuard at the System Level on Windows
|
||||||
|
|
||||||
|
In Windows, you need to configure AdGuard for each network adapter you want to use.
|
||||||
|
|
||||||
|
- Go to _Home > Network & Internet_, then select the network adapter to modify
|
||||||
|
- Click _Edit DNS_ (sometimes under _Hardware Properties_)
|
||||||
|
- Choose `Manual`
|
||||||
|
- Enable IPv4
|
||||||
|
- Enter your server’s public IP (the one accessible from the internet)
|
||||||
|
- Enable _DNS over HTTPS (manual template)_
|
||||||
|
- Disable _Fallback to plaintext_
|
||||||
|
- Save
|
||||||
|
|
||||||
|
All programs using that network adapter will now be filtered by AdGuard.
|
||||||
|
|
||||||
|
## Add Filters
|
||||||
|
---
|
||||||
|
|
||||||
|
- Go to the settings and change the filters.
|
||||||
+47
-139
@@ -1,10 +1,16 @@
|
|||||||
---
|
---
|
||||||
|
navigation: true
|
||||||
title: Vaultwarden
|
title: Vaultwarden
|
||||||
description: Install Vaultwarden, a self-hosted Bitwarden-compatible password manager to replace Google or Apple password managers across all your devices.
|
description: Install Vaultwarden, a self-hosted Bitwarden-compatible password manager to replace Google or Apple password managers across all your devices.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
---
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
# Vaultwarden
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 __Goals:__ Install [Vaultwarden](https://github.com/dani-garcia/vaultwarden) to manage your passwords across all your devices (a replacement for Google or Apple password managers).
|
||||||
|
::
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@@ -13,26 +19,21 @@ description: Install Vaultwarden, a self-hosted Bitwarden-compatible password ma
|
|||||||
Vaultwarden is a fork of [Bitwarden](https://bitwarden.com/fr-fr/help/).
|
Vaultwarden is a fork of [Bitwarden](https://bitwarden.com/fr-fr/help/).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
::file-tree
|
|
||||||
---
|
---
|
||||||
tree:
|
Folder structure:
|
||||||
/:
|
|
||||||
- srv:
|
|
||||||
- docker:
|
|
||||||
- vaultwarden:
|
|
||||||
- data/
|
|
||||||
- compose.yaml
|
|
||||||
- .env
|
|
||||||
---
|
|
||||||
::
|
|
||||||
|
|
||||||
::steps{level="3"}
|
```sh
|
||||||
### Deploy the stack
|
root
|
||||||
|
└── docker
|
||||||
|
└── vaultwarden
|
||||||
|
├── data
|
||||||
|
├── compose.yaml
|
||||||
|
└── .env
|
||||||
|
```
|
||||||
|
|
||||||
Open Dockge, click on `compose`, name the stack `vaultwarden`, and paste the following:
|
Open Dockge, click on `compose`, name the stack `vaultwarden`, and paste the following:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
vaultwarden:
|
vaultwarden:
|
||||||
@@ -61,11 +62,10 @@ services:
|
|||||||
- SIGNUPS_VERIFY_RESEND_LIMIT=5
|
- SIGNUPS_VERIFY_RESEND_LIMIT=5
|
||||||
```
|
```
|
||||||
|
|
||||||
::tip{icon=""}
|
::alert{type="success"}
|
||||||
✨ __Tip:__ Add the Watchtower label in each container to automate updates
|
✨ __Tip:__ Add the Watchtower label in each container to automate updates
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
vaultwarden:
|
vaultwarden:
|
||||||
#...
|
#...
|
||||||
@@ -74,11 +74,9 @@ services:
|
|||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
### Set your environment variables
|
|
||||||
|
|
||||||
Next, generate a password hash to put in the `TOKEN` variable in `.env`:
|
Next, generate a password hash to put in the `TOKEN` variable in `.env`:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
echo -n 'yourpassword' | argon2 "$(openssl rand -base64 32)" -e -id -k 65540 -t 3 -p 4
|
echo -n 'yourpassword' | argon2 "$(openssl rand -base64 32)" -e -id -k 65540 -t 3 -p 4
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -86,7 +84,7 @@ Copy the result securely.
|
|||||||
|
|
||||||
In the `.env` file, enter the following variables:
|
In the `.env` file, enter the following variables:
|
||||||
|
|
||||||
```properties [.env]
|
```properties
|
||||||
URL=
|
URL=
|
||||||
TOKEN=
|
TOKEN=
|
||||||
```
|
```
|
||||||
@@ -100,33 +98,28 @@ Then deploy the container.
|
|||||||
|
|
||||||
Recently, Vaultwarden requires SSL to be accessed, which prevents access via a local IP. We'll expose it with SWAG, which provides an SSL certificate.
|
Recently, Vaultwarden requires SSL to be accessed, which prevents access via a local IP. We'll expose it with SWAG, which provides an SSL certificate.
|
||||||
|
|
||||||
### Done!
|
::alert{type="danger"}
|
||||||
::
|
:::list{type="danger"}
|
||||||
|
- __If it fails:__ check your firewall rules.
|
||||||
::caution
|
:::
|
||||||
|
|
||||||
__If it fails:__ check your firewall rules.
|
|
||||||
::
|
::
|
||||||
|
|
||||||
## Exposing Vaultwarden with SWAG
|
## Exposing Vaultwarden with SWAG
|
||||||
|
---
|
||||||
The main benefit of Vaultwarden is being able to access it remotely from any device. We'll expose it through [SWAG](/serveex/core/swag).
|
The main benefit of Vaultwarden is being able to access it remotely from any device. We'll expose it through [SWAG](/serveex/core/swag).
|
||||||
|
|
||||||
::note{icon="" to="/general/networking/nat"}
|
::alert{type="info"}
|
||||||
📋 __Before you start:__ Make sure you've created a DNS subdomain like `vault.yourdomain.com` with `CNAME` pointing to `yourdomain.com` and (unless using Cloudflare Zero Trust) that you've forwarded port `443` from your router to your server's `443` via **NAT rules**.
|
✨ __Before you start:__ Make sure you've created a DNS subdomain like `vault.yourdomain.com` with `CNAME` pointing to `yourdomain.com` and (unless using Cloudflare Zero Trust) that you've forwarded port `443` from your router to your server's `443` via [NAT rules](/general/networking/nat).
|
||||||
::
|
::
|
||||||
|
|
||||||
::steps{level="3"}
|
|
||||||
### Add Vaultwarden's network to SWAG
|
|
||||||
|
|
||||||
In Dockge, go to the SWAG stack and edit the compose file to add the Vaultwarden network:
|
In Dockge, go to the SWAG stack and edit the compose file to add the Vaultwarden network:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
swag:
|
swag:
|
||||||
container_name: # ...
|
container_name: # ...
|
||||||
# ...
|
# ...
|
||||||
networks: # Connects container to custom network
|
networks: # Connects container to custom network
|
||||||
# ...
|
# ...
|
||||||
- vaultwarden # Name of the declared network
|
- vaultwarden # Name of the declared network
|
||||||
|
|
||||||
@@ -137,28 +130,27 @@ networks: # Defines the custom network
|
|||||||
external: true
|
external: true
|
||||||
```
|
```
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
We're assuming the network name is `vaultwarden_default`. You can check connectivity by visiting the SWAG dashboard at http://yourserverip:81.
|
- We're assuming the network name is `vaultwarden_default`. You can check connectivity by visiting the SWAG dashboard at http://yourserverip:81.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
Restart the stack by clicking "Deploy" and wait for SWAG to be fully operational.
|
Restart the stack by clicking "Deploy" and wait for SWAG to be fully operational.
|
||||||
|
|
||||||
### Create the subdomain.conf file
|
|
||||||
|
|
||||||
In SWAG's config folder, create the file `vault.subdomain.conf`:
|
In SWAG's config folder, create the file `vault.subdomain.conf`:
|
||||||
|
|
||||||
::tip{icon="" to="/serveex/files/file-browser-quantum"}
|
::alert{type="success"}
|
||||||
✨ __Tip:__ Use **File Browser Quantum** to navigate and edit files instead of using terminal commands.
|
✨ __Tip:__ Use [File Browser](/serveex/files/file-browser) to navigate and edit files instead of using terminal commands.
|
||||||
::
|
::
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo nano /srv/docker/swag/config/nginx/proxy-confs/vault.subdomain.conf
|
sudo vi /docker/swag/config/nginx/proxy-confs/vault.subdomain.conf
|
||||||
```
|
```
|
||||||
|
|
||||||
And paste the following configuration:
|
Press `i` to edit, and paste the following configuration:
|
||||||
|
|
||||||
```nginx [vault.subdomain.conf]
|
```nginx
|
||||||
server {
|
server {
|
||||||
listen 443 ssl;
|
listen 443 ssl;
|
||||||
listen [::]:443 ssl;
|
listen [::]:443 ssl;
|
||||||
@@ -242,96 +234,12 @@ server {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Esc`, then type `:x` and press `Enter` to save and exit.
|
||||||
|
|
||||||
### Done!
|
And there you go — Vaultwarden is now exposed! Visit `https://vault.yourdomain.com/admin` to access the admin panel and paste the password you specified when generatique the `ADMIN_TOKEN`. For more information, see the [Bitwarden documentation](https://bitwarden.com/help/).
|
||||||
::
|
|
||||||
|
|
||||||
And there you go! Vaultwarden is now exposed! Visit `https://vault.yourdomain.com/admin` to access the admin panel and paste the password you specified when generating the `ADMIN_TOKEN`. For more information, see the [Bitwarden documentation](https://bitwarden.com/help/).
|
|
||||||
|
|
||||||
Don't forget to install Bitwarden browser extensions (they work with Vaultwarden) for [Chrome](https://chromewebstore.google.com/detail/gestionnaire-de-mots-de-p/nngceckbapebfimnlniiiahkandclblb) and [Firefox](https://addons.mozilla.org/fr/firefox/addon/bitwarden-password-manager/), as well as [iOS](https://apps.apple.com/fr/app/bitwarden/id1137397744) and [Android](https://play.google.com/store/apps/details?id=com.x8bit.bitwarden&hl=fr) apps to sync your passwords.
|
Don't forget to install Bitwarden browser extensions (they work with Vaultwarden) for [Chrome](https://chromewebstore.google.com/detail/gestionnaire-de-mots-de-p/nngceckbapebfimnlniiiahkandclblb) and [Firefox](https://addons.mozilla.org/fr/firefox/addon/bitwarden-password-manager/), as well as [iOS](https://apps.apple.com/fr/app/bitwarden/id1137397744) and [Android](https://play.google.com/store/apps/details?id=com.x8bit.bitwarden&hl=fr) apps to sync your passwords.
|
||||||
|
|
||||||
## Protecting Vaultwarden with Pocket ID
|
::alert{type="success"}
|
||||||
Unlike most apps on this site, Vaultwarden supports OIDC natively, so there's no need for TinyAuth's forward-auth proxy or Authentik's reverse-proxy trick here: Vaultwarden itself can require an OIDC login before letting anyone into the vault.
|
✨ __Tip:__ You can protect this app with Authentik by opening `tools.subdomain.conf` and removing the `#` in front of `include /config/nginx/authentik-server.conf;` and `include /config/nginx/authentik-location.conf;`. Don't forget to [create an application and provider in Authentik](/serveex/security/authentik#protecting-an-app-via-reverse-proxy).
|
||||||
|
::
|
||||||
::steps{level="3"}
|
|
||||||
### Register Vaultwarden as an OIDC client
|
|
||||||
|
|
||||||
[Register an OIDC client in Pocket ID](/serveex/security/pocket-id#registering-an-oidc-client) (or in Authentik, or any other OIDC provider) using this callback URL:
|
|
||||||
|
|
||||||
```text
|
|
||||||
https://vault.yourdomain.com/identity/connect/oidc-signin
|
|
||||||
```
|
|
||||||
|
|
||||||
### Add the SSO variables
|
|
||||||
|
|
||||||
Edit Vaultwarden's `.env` file:
|
|
||||||
|
|
||||||
```bash [Terminal]
|
|
||||||
sudo nano /srv/docker/vaultwarden/.env
|
|
||||||
```
|
|
||||||
|
|
||||||
Add the following:
|
|
||||||
|
|
||||||
```properties [.env]
|
|
||||||
SSO_ENABLED=true
|
|
||||||
SSO_ONLY=true
|
|
||||||
SSO_AUTHORITY=https://id.yourdomain.com
|
|
||||||
SSO_CLIENT_ID=
|
|
||||||
SSO_CLIENT_SECRET=
|
|
||||||
```
|
|
||||||
|
|
||||||
| Variable | Value |
|
|
||||||
|----------|-------|
|
|
||||||
| `SSO_AUTHORITY`{lang=properties} | Your OIDC provider's public URL (Pocket ID here) |
|
|
||||||
| `SSO_CLIENT_ID`{lang=properties} | The client ID copied from your provider |
|
|
||||||
| `SSO_CLIENT_SECRET`{lang=properties} | The client secret copied from your provider |
|
|
||||||
|
|
||||||
::note
|
|
||||||
|
|
||||||
Set `SSO_ONLY=false` instead if you'd rather keep the option to log in with a local master password too.
|
|
||||||
::
|
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
|
||||||
|
|
||||||
### Redeploy the stack
|
|
||||||
|
|
||||||
Redeploy Vaultwarden. Your next visit to `https://vault.yourdomain.com` will prompt for SSO login instead of (or alongside) the local master password.
|
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
::note{to="https://github.com/dani-garcia/vaultwarden/wiki/Enabling-SSO-support-using-OpenId-Connect"}
|
|
||||||
See the **Vaultwarden SSO wiki** for the full list of options, including per-organization enforcement and master password policies.
|
|
||||||
::
|
|
||||||
|
|
||||||
::::tip{icon=""}
|
|
||||||
✨ You can use [Authentik](/serveex/advanced/authentik) instead of Pocket ID:
|
|
||||||
|
|
||||||
:::collapsible{name="the Authentik setup steps"}
|
|
||||||
::steps{level="4"}
|
|
||||||
#### Create a scope mapping for the email claim
|
|
||||||
|
|
||||||
In Authentik, create a scope mapping named `email` with the expression `return {"email": request.user.email, "email_verified": True}` (Vaultwarden requires this claim).
|
|
||||||
|
|
||||||
#### Create the application and provider
|
|
||||||
|
|
||||||
Create an application and an OAuth2/OpenID Connect provider named `Vaultwarden`, with a redirect URI (type `Strict`) of `https://vault.yourdomain.com/identity/connect/oidc-signin`. Under Advanced protocol settings, set the access token validity to more than 5 minutes, replace the default email scope with your custom mapping, and add the `offline_access` scope mapping.
|
|
||||||
|
|
||||||
#### Set Vaultwarden's environment variables
|
|
||||||
|
|
||||||
Note the provider's __Client ID__, __Client Secret__, and __Slug__, then use them in Vaultwarden's `.env`:
|
|
||||||
|
|
||||||
```properties [.env]
|
|
||||||
SSO_AUTHORITY=https://authentik.yourdomain.com/application/o/<slug>/
|
|
||||||
SSO_CLIENT_ID=
|
|
||||||
SSO_CLIENT_SECRET=
|
|
||||||
SSO_SCOPES=email profile offline_access
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
:::
|
|
||||||
|
|
||||||
::::
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
navigation.title: Useful Apps
|
||||||
|
icon: lucide:award
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
icon: noto:microscope
|
||||||
|
navigation.redirect: /serveex/introduction
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
---
|
||||||
|
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>
|
||||||
|
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
icon: noto:computer-disk
|
||||||
|
navigation.redirect: /stockeex/introduction
|
||||||
+13
-6
@@ -1,16 +1,20 @@
|
|||||||
---
|
---
|
||||||
|
navigation: true
|
||||||
title: Nvidia Stock Bot
|
title: Nvidia Stock Bot
|
||||||
description: A Python bot that monitors GPU stock availability in real time and sends Discord alerts, built during the RTX 5000 series launch shortage.
|
description: A Python bot that monitors GPU stock availability in real time and sends Discord alerts — built during the RTX 5000 series launch shortage.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
---
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
|
||||||
|
# 🤖 Nvidia Stock Bot
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
---
|
||||||
|
|
||||||
For the past four years, the electronics hardware shortage has been relentless. Graphics cards are no exception. In 2020, I had to wait two months to get my RTX 3080. To manage it, I joined [JV Hardware](https://discord.gg/gxffg3GA96), where a small group of geeks had set up a bot that pinged users when GPUs became available.
|
For the past four years, the electronics hardware shortage has been relentless. Graphics cards are no exception. In 2020, I had to wait two months to get my RTX 3080. To manage it, I joined [JV Hardware](https://discord.gg/gxffg3GA96), where a small group of geeks had set up a bot that pinged users when GPUs became available.
|
||||||
|
|
||||||
Four years later and with 5,000 members on the server, the RTX 5000 series is being released. Yet, no working stock bot seems to exist. Not to mention a certain “influencer” who charges users for access to a bot that doesn’t even work. He manually copies alerts from other servers like ours, which have already solved the issue.
|
Four years later and with 5,000 members on the server, the RTX 5000 series is being released. Yet, no working stock bot seems to exist. Not to mention a certain “influencer” who charges users for access to a bot that doesn’t even work. He manually copies alerts from other servers like ours, which have already solved the issue.
|
||||||
|
|
||||||
Anyway, eager to get an RTX 5090 for my AI-dedicated machine, I decided it was time to dive into Python, with a little help from ChatGPT. Along with another member, KevOut, who helped guide me through the APIs and initial architecture, I ended up building a clean and functional bot that sends different kinds of Discord alerts, all deployable in a simple Docker container.
|
Anyway, eager to get an RTX 5090 for my AI-dedicated machine, I decided it was time to dive into Python—with a little help from ChatGPT. Along with another member, KevOut, who helped guide me through the APIs and initial architecture, I ended up building a clean and functional bot that sends different kinds of Discord alerts—all deployable in a simple Docker container.
|
||||||
|
|
||||||
After many setbacks, I went from this:
|
After many setbacks, I went from this:
|
||||||
|
|
||||||
@@ -28,6 +32,9 @@ And I was also lucky enough to be referenced in the famous [selfhost newsletter]
|
|||||||
|
|
||||||
More info directly on the repo:
|
More info directly on the repo:
|
||||||
|
|
||||||
::card{title="🐋 __Nvidia Stock Bot__" to="https://git.djeex.fr/Djeex/nvidia-stock-bot" target="_blank"}
|
::card
|
||||||
Nvidia GPU stock alert bot
|
#title
|
||||||
|
🐋 __Nvidia Stock Bot__
|
||||||
|
#description
|
||||||
|
[Nvidia GPU stock alert bot](https://git.djeex.fr/Djeex/nvidia-stock-bot)
|
||||||
::
|
::
|
||||||
+21
-15
@@ -1,33 +1,39 @@
|
|||||||
---
|
---
|
||||||
title: AdGuard CIDRE
|
navigation: true
|
||||||
|
title: Adguard CIDRE
|
||||||
description: A Python script to sync AdGuard Home CIDR allowlists automatically, securing your self-hosted DNS server exposed to the internet.
|
description: A Python script to sync AdGuard Home CIDR allowlists automatically, securing your self-hosted DNS server exposed to the internet.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
|
||||||
|
# 🤖 Adguard CIDRE Sync
|
||||||
---
|
---
|
||||||
|
|
||||||
|
Adguard Home is a fantastic solution for DNS-level ad blocking and rewriting requests—perfect for removing ISP DNS trackers or intrusive ads.
|
||||||
|
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
It works great locally, but if you want all your devices (even on the go) to benefit, you’ll need to expose Adguard to the internet. Unfortunately, that means anyone can use it, potentially overloading your €1/month remote VPS.
|
||||||
|
|
||||||
AdGuard Home is a fantastic solution for DNS-level ad blocking and rewriting requests, perfect for removing ISP DNS trackers or intrusive ads.
|
Adguard allows whitelisting or blacklisting clients. The problem? To whitelist a client, you need their IP—but for mobile phones, that IP changes often. Instead of trying to whitelist ever-changing IPs, the better approach is to block broader IP ranges by region.
|
||||||
|
|
||||||
It works great locally, but if you want all your devices (even on the go) to benefit, you’ll need to expose AdGuard to the internet. Unfortunately, that means anyone can use it, potentially overloading your €1/month remote VPS.
|
CIDRE is a tool that syncs geo-targeted IP ranges with firewalls. Instead of running CIDRE with a full firewall stack on the remote server, I figured I could just import those regularly updated IP ranges into Adguard’s blocklist.
|
||||||
|
|
||||||
AdGuard allows whitelisting or blacklisting clients. The problem? To whitelist a client, you need their IP, but for mobile phones, that IP changes often. Instead of trying to whitelist ever-changing IPs, the better approach is to block broader IP ranges by region.
|
Thus, Adguard CIDRE Sync was born: a container that syncs Adguard’s blocklist with CIDRE’s updated IP ranges on a schedule of your choosing.
|
||||||
|
|
||||||
CIDRE is a tool that syncs geo-targeted IP ranges with firewalls. Instead of running CIDRE with a full firewall stack on the remote server, I figured I could just import those regularly updated IP ranges into AdGuard’s blocklist.
|
|
||||||
|
|
||||||
Thus, AdGuard CIDRE Sync was born: a container that syncs AdGuard’s blocklist with CIDRE’s updated IP ranges on a schedule of your choosing.
|
|
||||||
|
|
||||||
The idea is to:
|
The idea is to:
|
||||||
|
- Backup Adguard’s config file on first run (original untouched version saved)
|
||||||
- Backup AdGuard’s config file on first run (original untouched version saved)
|
|
||||||
- Download selected country IP ranges via an environment variable
|
- Download selected country IP ranges via an environment variable
|
||||||
- Let you manually add custom IPs via a file
|
- Let you manually add custom IPs via a file
|
||||||
- Concatenate, backup the config again (as the updated version), and inject the list into the correct blocklist section
|
- Concatenate, backup the config again (as the updated version), and inject the list into the correct blocklist section
|
||||||
- Reload AdGuard by restarting the container (using Docker socket proxy for limited permissions)
|
- Reload Adguard by restarting the container (using Docker socket proxy for limited permissions)
|
||||||
|
|
||||||
All fully autonomous, with frequency set via environment variable in the `docker-compose` config.
|
All fully autonomous, with frequency set via environment variable in the `docker-compose` config.
|
||||||
|
|
||||||
More info directly on the repo:
|
More info directly on the repo:
|
||||||
|
|
||||||
::card{title="🐋 __AdGuard CIDRE Sync__" to="https://git.djeex.fr/Djeex/adguard-cidre" target="_blank"}
|
::card
|
||||||
AdGuard blocklist sync bot
|
#title
|
||||||
::
|
🐋 __Adguard CIDRE Sync__
|
||||||
|
#description
|
||||||
|
[Adguard blocklist sync bot](https://git.djeex.fr/Djeex/adguard-cidre)
|
||||||
|
::
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
---
|
||||||
|
navigation: true
|
||||||
|
title: Lumeex
|
||||||
|
description: Lumeex is a static photo gallery site generator built with Python — minimalist, lightweight, and fully customizable without a CMS.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/logo.svg" alt="Lumeex Screenshot" width="300"/>
|
||||||
|
</div>
|
||||||
|
<p/>
|
||||||
|
<div align="center">
|
||||||
|
<p>Yet another minimalist, lightweight photo gallery static site generator.</p>
|
||||||
|
</div>
|
||||||
|
</p>
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/lumeex.png" alt="Lumeex Screenshot" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Amateur photographer that I am, I spent several weeks looking for a framework with a photo gallery that could outshine Instagram. I wanted something that showcased the photos rather than the author, and that made every visit unique by loading the images in random order—while still allowing filtering and sorting by tag or combinations of tags.
|
||||||
|
|
||||||
|
In the end, I found nothing that did exactly what I wanted. And when something came close, it was always through heavy, bloated CMS platforms. So I decided to make a static site by hand, the old-school way, with Notepad++. Being fairly comfortable with HTML/CSS and a bit of JavaScript, I quickly came up with something nice during my vacation, between beach sessions. After all, a good craftsman should have good tools—and there’s no better tool than one you make yourself.
|
||||||
|
|
||||||
|
Then I thought it might be a good idea to automate certain tasks—like generating favicon formats, resizing and converting images, creating the gallery automatically instead of entering everything by hand, and generating `robots.txt` and `sitemap` files… so I turned back to Python.
|
||||||
|
|
||||||
|
Eventually, after getting good results, I figured I might as well go all the way: build a complete framework for generating a static site photo gallery, where all you need to do is fill in your site’s information in a config file and tweak the visuals a bit—without touching the code.
|
||||||
|
|
||||||
|
That’s how **Lum[eex]{style="color: #1ad6ff"}** was born.
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="https://git.djeex.fr/Djeex/lumeex/raw/branch/main/illustration/lumeex-webui.png" alt="Lumeex Screenshot" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
::card-grid{grid-template-columns="repeat(2, minmax(0, 1fr));"}
|
||||||
|
#title
|
||||||
|
Et voilà!
|
||||||
|
|
||||||
|
#root
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
|
||||||
|
#default
|
||||||
|
|
||||||
|
::card{icon=noto:open-book}
|
||||||
|
#title
|
||||||
|
__Documentation__
|
||||||
|
#description
|
||||||
|
[Check out the doc](https://lumeex.djeex.fr)
|
||||||
|
::
|
||||||
|
|
||||||
|
::card{icon=simple-icons:gitea style="color: #9ee773;"}
|
||||||
|
#title
|
||||||
|
__Repository__
|
||||||
|
#description
|
||||||
|
[See the repo](https://git.djeex.fr/Djeex/lumeex)
|
||||||
|
::
|
||||||
|
|
||||||
|
::card{icon=fluent-color:design-ideas-48}
|
||||||
|
#title
|
||||||
|
__Demo__
|
||||||
|
#description
|
||||||
|
[Explore the demo](https://modern.djeex.fr)
|
||||||
|
::
|
||||||
|
::
|
||||||
+27
-16
@@ -1,18 +1,22 @@
|
|||||||
---
|
---
|
||||||
|
navigation: true
|
||||||
title: Instameex
|
title: Instameex
|
||||||
description: Instameex is a Docker-based tool to merge SDR and HDR photo exports into a proper gain-map JPEG ready for Instagram HDR upload.
|
description: Instameex is a Docker-based tool to merge SDR and HDR photo exports into a proper gain-map JPEG ready for Instagram HDR upload.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
---
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
|
||||||
|
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="/img/nonsense/instameex-logo-long.svg" alt="Instameex logo" width="300"/>
|
<img src="https://git.djeex.fr/Djeex/instameex/raw/branch/main/src/assets/img/logo-long.svg" alt="Lumeex Screenshot" width="300"/>
|
||||||
</div>
|
</div>
|
||||||
|
<p/>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<p>Mix your SDR and HDR exports into an Instagram-ready HDR photo.</p>
|
<p>Mix your SDR and HDR exports into an Instagram-ready HDR photo.</p>
|
||||||
</div>
|
</div>
|
||||||
|
</p>
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="/img/nonsense/instameex-illustration.png" width="640" alt="Instameex illustration" />
|
<img src="https://git.djeex.fr/Djeex/instameex/raw/branch/main/illustration/instameex-illustration.png" width="640" alt="Instameex Screenshot" />
|
||||||
</div>
|
</div>
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -30,18 +34,25 @@ In short, here is what my workflow now looks like for posting on Instagram:
|
|||||||
Allow me to introduce **Instam[eex]{style="color: #1ad6ff"}**
|
Allow me to introduce **Instam[eex]{style="color: #1ad6ff"}**
|
||||||
|
|
||||||
---
|
---
|
||||||
### And here is the result
|
::card-grid{grid-template-columns="repeat(2, minmax(0, 1fr));"}
|
||||||
|
#title
|
||||||
|
And here is the result
|
||||||
|
|
||||||
:::div{class="relative"}
|
#root
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
:::
|
|
||||||
|
|
||||||
::card-group
|
#default
|
||||||
::card{title="🐋 __Instameex__" to="https://git.djeex.fr/Djeex/instameex" target="_blank"}
|
::card
|
||||||
Open the repository
|
#title
|
||||||
::
|
🐋 __Instameex__
|
||||||
|
#description
|
||||||
::card{title="🌍 __Online version__" to="https://instameex.djeex.fr" target="_blank"}
|
[Open the repository](https://git.djeex.fr/Djeex/instameex)
|
||||||
Convert online
|
::
|
||||||
::
|
|
||||||
::
|
::card
|
||||||
|
#title
|
||||||
|
🌍 __Online version__
|
||||||
|
#description
|
||||||
|
[Convert online](https://instameex.djeex.fr)
|
||||||
|
::
|
||||||
|
::
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
navigation.title: Python
|
||||||
|
icon: lucide:file-code-2
|
||||||
+23
-23
@@ -1,28 +1,29 @@
|
|||||||
---
|
---
|
||||||
|
navigation: true
|
||||||
title: Servarr corrector
|
title: Servarr corrector
|
||||||
description: A bash script to detect and fix duplicate media files in Sonarr and Radarr libraries by replacing copies with hardlinks to reclaim disk space.
|
description: A bash script to detect and fix duplicate media files in Sonarr and Radarr libraries by replacing copies with hardlinks to reclaim disk space.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
# Servarr duplicates corrector
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
|
||||||
|
|
||||||
Six months after downloading terabytes of media, I realized that Sonarr and Radarr were copying them into my Plex library instead of creating hardlinks. This happens due to a counterintuitive mechanism: if you mount multiple folders in Sonarr/Radarr, it sees them as different filesystems and thus cannot create hardlinks. That’s why you should mount only one parent folder containing all child folders (like `downloads`, `movies`, `tvseries` inside a `media` parent folder).
|
Six months after downloading terabytes of media, I realized that Sonarr and Radarr were copying them into my Plex library instead of creating hardlinks. This happens due to a counterintuitive mechanism: if you mount multiple folders in Sonarr/Radarr, it sees them as different filesystems and thus cannot create hardlinks. That’s why you should mount only one parent folder containing all child folders (like `downloads`, `movies`, `tvseries` inside a `media` parent folder).
|
||||||
|
|
||||||
So I restructured my directories, manually updated every path in qBittorrent, Plex, and others. The last challenge was finding a way to detect existing duplicates, delete them, and automatically create hardlinks instead, to save space.
|
So I restructured my directories, manually updated every path in Qbittorrent, Plex, and others. The last challenge was finding a way to detect existing duplicates, delete them, and automatically create hardlinks instead—to save space.
|
||||||
|
|
||||||
::file-tree
|
My directory structure:
|
||||||
---
|
|
||||||
label: My directory structure
|
```sh
|
||||||
tree:
|
.
|
||||||
.:
|
└── media
|
||||||
- media:
|
├── seedbox
|
||||||
- seedbox/
|
├── radarr
|
||||||
- radarr:
|
│ └── tv-radarr
|
||||||
- tv-radarr/
|
├── movies
|
||||||
- movies/
|
└── tvseries
|
||||||
- tvseries/
|
```
|
||||||
---
|
|
||||||
::
|
|
||||||
|
|
||||||
The originals are in `seedbox` and must not be modified to keep seeding. The copies (duplicates) are in `movies` and `tvseries`. To complicate things, there are also unique originals in `movies` and `tvseries`. And within those, there can be subfolders, sub-subfolders, etc.
|
The originals are in `seedbox` and must not be modified to keep seeding. The copies (duplicates) are in `movies` and `tvseries`. To complicate things, there are also unique originals in `movies` and `tvseries`. And within those, there can be subfolders, sub-subfolders, etc.
|
||||||
|
|
||||||
@@ -38,11 +39,11 @@ Yes, I asked ChatGPT and Qwen3 (which I host on a dedicated AI machine). Natural
|
|||||||
|
|
||||||
In the end, I only needed to find `.mkv` files, and duplicates have the exact same name as the originals, which simplifies things a lot. A simple Bash script would do the job.
|
In the end, I only needed to find `.mkv` files, and duplicates have the exact same name as the originals, which simplifies things a lot. A simple Bash script would do the job.
|
||||||
|
|
||||||
Spare you the endless Q&A with ChatGPT, I was disappointed. Qwen3 was much cleaner. ChatGPT kept pushing awk-based solutions, which fail on paths with spaces. With Qwen’s help and dropping awk, the results improved significantly.
|
Spare you the endless Q&A with ChatGPT—I was disappointed. Qwen3 was much cleaner. ChatGPT kept pushing awk-based solutions, which fail on paths with spaces. With Qwen’s help and dropping awk, the results improved significantly.
|
||||||
|
|
||||||
To test, I first asked for a script that only lists and compares:
|
To test, I first asked for a script that only lists and compares:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Create an associative array to store duplicates
|
# Create an associative array to store duplicates
|
||||||
@@ -84,11 +85,11 @@ With `awk`, it would’ve stopped at `/media/seedbox/sonarr/Serie`. I’m far fr
|
|||||||
|
|
||||||
Once I verified the output, I asked for a complete script: compare, delete duplicates, create hardlinks.
|
Once I verified the output, I asked for a complete script: compare, delete duplicates, create hardlinks.
|
||||||
|
|
||||||
Again, ChatGPT disappointed. Despite my requests, it created hardlinks *before* deleting the duplicates, effectively linking and then deleting the link (though the original is kept). Not helpful.
|
Again, ChatGPT disappointed. Despite my requests, it created hardlinks *before* deleting the duplicates—effectively linking and then deleting the link (though the original is kept). Not helpful.
|
||||||
|
|
||||||
Quick stopover to Qwen3, RTX 5090 in overdrive, and bam, much better result. Yes, it kept ChatGPT-style emojis, but here it is:
|
Quick stopover to Qwen3, RTX 5090 in overdrive, and bam—much better result. Yes, it kept ChatGPT-style emojis, but here it is:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
echo "🔍 Step 1: Indexing original files in /media/seedbox..."
|
echo "🔍 Step 1: Indexing original files in /media/seedbox..."
|
||||||
@@ -135,7 +136,6 @@ echo "✅ Done."
|
|||||||
```
|
```
|
||||||
|
|
||||||
So, in conclusion, I:
|
So, in conclusion, I:
|
||||||
|
|
||||||
- Learned many Bash subtleties
|
- Learned many Bash subtleties
|
||||||
- Learned never to blindly copy-paste a ChatGPT script without understanding and dry-running it
|
- Learned never to blindly copy-paste a ChatGPT script without understanding and dry-running it
|
||||||
- Learned that Qwen on a RTX 5090 is more coherent than ChatGPT-4o on server farms (not even mentioning “normal” ChatGPT)
|
- Learned that Qwen on a RTX 5090 is more coherent than ChatGPT-4o on server farms (not even mentioning “normal” ChatGPT)
|
||||||
+7
-4
@@ -1,17 +1,20 @@
|
|||||||
---
|
---
|
||||||
|
navigation: true
|
||||||
title: LUKS Backup
|
title: LUKS Backup
|
||||||
description: A bash script to automatically dump LUKS headers from all encrypted disks, identify them by serial number, and store them in an encrypted archive.
|
description: A bash script to automatically dump LUKS headers from all encrypted disks, identify them by serial number, and store them in an encrypted archive.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
---
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
|
||||||
|
# Backup of LUKS Headers for Encrypted Disks/Volumes
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
---
|
||||||
|
|
||||||
I recently realized that having just the password is not enough to unlock a LUKS volume after a failure or corruption. I learned how to dump the LUKS headers from disks/volumes and to use the serial numbers along with partition names to accurately identify which header corresponds to which disk/partition (I have 10 of them!).
|
I recently realized that having just the password is not enough to unlock a LUKS volume after a failure or corruption. I learned how to dump the LUKS headers from disks/volumes and to use the serial numbers along with partition names to accurately identify which header corresponds to which disk/partition (I have 10 of them!).
|
||||||
|
|
||||||
After struggling to do this manually, I asked Qwen3 (an LLM running on my RTX 5090) to create a script that automates the listing and identification of disks, dumps the headers, and stores them in an encrypted archive ready to be backed up on my backup server.
|
After struggling to do this manually, I asked Qwen3 (an LLM running on my RTX 5090) to create a script that automates the listing and identification of disks, dumps the headers, and stores them in an encrypted archive ready to be backed up on my backup server.
|
||||||
|
|
||||||
This script:
|
This script:
|
||||||
|
|
||||||
* Lists and identifies disks with their serial numbers
|
* Lists and identifies disks with their serial numbers
|
||||||
* Lists partitions
|
* Lists partitions
|
||||||
* Dumps headers into a secured folder under `/root`
|
* Dumps headers into a secured folder under `/root`
|
||||||
@@ -20,7 +23,7 @@ This script:
|
|||||||
* Encrypts the archive with that password
|
* Encrypts the archive with that password
|
||||||
* Deletes the unencrypted archive
|
* Deletes the unencrypted archive
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
# Directory where LUKS headers will be backed up
|
# Directory where LUKS headers will be backed up
|
||||||
+14
-7
@@ -1,10 +1,15 @@
|
|||||||
---
|
---
|
||||||
|
navigation: true
|
||||||
title: Socat Proxy
|
title: Socat Proxy
|
||||||
description: Use socat to proxy the Docker socket through Docker Socket Proxy, allowing Beszel to collect container stats without exposing the full Docker socket.
|
description: Use socat to proxy the Docker socket through Docker Socket Proxy, allowing Beszel to collect container stats without exposing the full Docker socket.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
|
||||||
|
# Socat Proxy
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
|
||||||
This project addresses a common use case:
|
This project addresses a common use case:
|
||||||
|
|
||||||
- I have [Beszel](https://beszel.dev/), a monitoring container running in host mode, which requires access to the Docker socket to collect container statistics.
|
- I have [Beszel](https://beszel.dev/), a monitoring container running in host mode, which requires access to the Docker socket to collect container statistics.
|
||||||
@@ -23,12 +28,11 @@ With this setup, Docker Socket Proxy communicates with Socat Proxy in their isol
|
|||||||
|
|
||||||
In short:
|
In short:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
For example, with Beszel, the configuration would look like this:
|
For example, with Beszel, the configuration would look like this:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
socat-proxy:
|
socat-proxy:
|
||||||
image: git.djeex.fr/djeex/socat-proxy:latest
|
image: git.djeex.fr/djeex/socat-proxy:latest
|
||||||
@@ -77,6 +81,9 @@ services:
|
|||||||
|
|
||||||
More information is available on the repository:
|
More information is available on the repository:
|
||||||
|
|
||||||
::card{title="🐋 __Socat Proxy__" to="https://git.djeex.fr/Djeex/socat-proxy" target="_blank"}
|
::card
|
||||||
A lightweight bind-mount socket proxy
|
#title
|
||||||
|
🐋 **Socat Proxy**
|
||||||
|
#description
|
||||||
|
[A lightweight bind-mount socket proxy](https://git.djeex.fr/Djeex/socat-proxy)
|
||||||
::
|
::
|
||||||
@@ -1,10 +1,14 @@
|
|||||||
---
|
---
|
||||||
|
navigation: true
|
||||||
title: HotDisk
|
title: HotDisk
|
||||||
description: A bash script that monitors hard drive temperatures and automatically shuts down the server when disks stay above a safe threshold for too long.
|
description: A bash script that monitors hard drive temperatures and automatically shuts down the server when disks stay above a safe threshold for too long.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
---
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
|
||||||
|
# HotDisk
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
---
|
||||||
|
|
||||||
When you have a NAS with several drives sitting in a laundry room, temperatures can quickly rise.
|
When you have a NAS with several drives sitting in a laundry room, temperatures can quickly rise.
|
||||||
Hard drives are very sensitive to heat and can suffer serious damage if they exceed a certain temperature threshold for too long.
|
Hard drives are very sensitive to heat and can suffer serious damage if they exceed a certain temperature threshold for too long.
|
||||||
@@ -32,7 +36,7 @@ The installer also lets you configure various parameters:
|
|||||||
| `DISCORD_WEBHOOK` | Discord webhook URL for notifications | _Required_ |
|
| `DISCORD_WEBHOOK` | Discord webhook URL for notifications | _Required_ |
|
||||||
|
|
||||||
It also runs another script that configures **logrotate** with the parameters defined above.
|
It also runs another script that configures **logrotate** with the parameters defined above.
|
||||||
Finally, the installer can even be executed directly via a simple `curl` command followed by one last setup script, perfect for the laziest of us.
|
Finally, the installer can even be executed directly via a simple `curl` command followed by one last setup script — perfect for the laziest of us.
|
||||||
|
|
||||||
I also had to handle several tricky cases: running as root without sudo, using sudo directly, running as a non-sudo user, missing dependencies, permission issues, file creation errors, disk data reading errors, and more.
|
I also had to handle several tricky cases: running as root without sudo, using sudo directly, running as a non-sudo user, missing dependencies, permission issues, file creation errors, disk data reading errors, and more.
|
||||||
|
|
||||||
@@ -40,6 +44,9 @@ Concurrent access to the status file also had to be managed carefully.
|
|||||||
|
|
||||||
More details are available directly on the repository:
|
More details are available directly on the repository:
|
||||||
|
|
||||||
::card{title="📜 __HotDisk__" to="https://git.djeex.fr/Djeex/hotdisk" target="_blank"}
|
::card
|
||||||
Keep your drives cool!
|
#title
|
||||||
|
📜 __HotDisk__
|
||||||
|
#description
|
||||||
|
[Keep your drives cool!](https://git.djeex.fr/Djeex/hotdisk)
|
||||||
::
|
::
|
||||||
+15
-9
@@ -1,16 +1,20 @@
|
|||||||
---
|
---
|
||||||
|
navigation: true
|
||||||
title: Backrest Docker Stop
|
title: Backrest Docker Stop
|
||||||
description: A bash script that stops Docker containers before a Backrest backup runs and restarts them after, ensuring safe database backups without complex dumps.
|
description: A bash script that stops Docker containers before a Backrest backup runs and restarts them after — ensuring safe database backups without complex dumps.
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
|
||||||
|
# Backrest Docker Stop
|
||||||
---
|
---
|
||||||
|
|
||||||
|
[Backrest](https://github.com/garethgeorge/backrest) is a fantastic backup tool. In the case of [Serveex](https://docu.djeex.fr/serveex/introduction), most of the data that needs to be backed up consists of containers, and those containers often include databases.
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
|
||||||
|
|
||||||
[Backrest](https://github.com/garethgeorge/backrest) is a fantastic backup tool. In the case of [Serveex](/serveex/introduction), most of the data that needs to be backed up consists of containers, and those containers often include databases.
|
|
||||||
|
|
||||||
The problem? You can’t safely back up a database while it’s running. There are plenty of complex solutions involving database dumps, but often the simplest method is to stop the containers, perform the backup, and then restart them.
|
The problem? You can’t safely back up a database while it’s running. There are plenty of complex solutions involving database dumps, but often the simplest method is to stop the containers, perform the backup, and then restart them.
|
||||||
|
|
||||||
**Backrest** doesn’t natively provide this functionality, but it does allow the execution of custom scripts triggered by events, for example, at the start and end of a backup plan. Our goal is to stop the containers whose databases need to be backed up when the backup plan starts, and restart them when the backup plan finishes. To achieve this, we’ll need a small Bash script and a secure connection between Backrest and the Docker socket, to enable the following sequence:
|
**Backrest** doesn’t natively provide this functionality, but it does allow the execution of custom scripts triggered by events, for example, at the start and end of a backup plan. Our goal is to stop the containers whose databases need to be backed up when the backup plan starts, and restart them when the backup plan finishes.To achieve this, we’ll need a small Bash script and a secure connection between Backrest and the Docker socket, to enable the following sequence:
|
||||||
|
|
||||||
- The backup plan starts
|
- The backup plan starts
|
||||||
- The event triggers the execution of a custom script
|
- The event triggers the execution of a custom script
|
||||||
@@ -20,13 +24,14 @@ The problem? You can’t safely back up a database while it’s running. There a
|
|||||||
- The event triggers another custom script
|
- The event triggers another custom script
|
||||||
- The script contacts Docker again, retrieves the same list, and restarts those containers
|
- The script contacts Docker again, retrieves the same list, and restarts those containers
|
||||||
|
|
||||||
|
|
||||||
## Securely Connecting Backrest and Docker
|
## Securely Connecting Backrest and Docker
|
||||||
|
|
||||||
To allow **Backrest** to communicate securely with Docker, we’ll use [Docker Socket Proxy](https://github.com/linuxserver/docker-socket-proxy).
|
To allow **Backrest** to communicate securely with Docker, we’ll use [Docker Socket Proxy](https://github.com/linuxserver/docker-socket-proxy).
|
||||||
This avoids exposing the full Docker socket and grants only the necessary permissions.
|
This avoids exposing the full Docker socket and grants only the necessary permissions.
|
||||||
Here’s an example Docker stack:
|
Here’s an example Docker stack:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
backrest:
|
backrest:
|
||||||
@@ -65,6 +70,7 @@ services:
|
|||||||
|
|
||||||
With this setup, Backrest can communicate with Docker safely and securely.
|
With this setup, Backrest can communicate with Docker safely and securely.
|
||||||
|
|
||||||
|
|
||||||
## The Scripts
|
## The Scripts
|
||||||
|
|
||||||
Below are the scripts to use for **Backrest**’s *start* and *end* backup events.
|
Below are the scripts to use for **Backrest**’s *start* and *end* backup events.
|
||||||
@@ -95,12 +101,12 @@ sleep 10
|
|||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
|
|
||||||
## The Label
|
## The Label
|
||||||
|
|
||||||
Once the scripts are in place and configured for the proper **Backrest** hooks, you just need to add the label `backrest.backup.stop=true` to the `compose.yaml` files of the containers that should stop and restart during backups:
|
Once the scripts are in place and configured for the proper **Backrest** hooks, you just need to add the label `backrest.backup.stop=true` to the `compose.yaml` files of the containers that should stop and restart during backups:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
your_service:
|
your_service:
|
||||||
...
|
...
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
navigation.title: Bash
|
||||||
|
icon: lucide:file-terminal
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
icon: noto:test-tube
|
||||||
|
navigation.title: My nonsense
|
||||||
+90
-103
@@ -1,23 +1,27 @@
|
|||||||
---
|
---
|
||||||
title: WireGuard 14
|
navigation: true
|
||||||
description: Archived guide to installing WireGuard VPN using linuxserver.io's older wireguard image, kept for reference only.
|
title: Wireguard 14
|
||||||
|
main:
|
||||||
|
fluid: false
|
||||||
---
|
---
|
||||||
|
:ellipsis{left=0px width=40rem top=10rem blur=140px}
|
||||||
|
# Wireguard
|
||||||
|
|
||||||
|
::alert{type="info"}
|
||||||
:ellipsis{left=0px width=40rem top=10rem blur=140px zIndex=60}
|
🎯 __Goals:__
|
||||||
|
- Install Wireguard
|
||||||
::note{to="/serveex/core/wireguard"}
|
- Configure clients
|
||||||
|
- Access the secure network
|
||||||
wg-easy 15 got a lot more complicated, "not so easy" anymore, arguably. That's exactly why this old v14 tutorial is worth keeping around: it's still the simplest way to get a WireGuard server running if you don't need what the new version adds.
|
|
||||||
::
|
::
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
---
|
||||||
Using a VPN allows remote access to a server’s local resources without exposing them to the internet. It’s 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 server’s local resources without exposing them to the internet. It’s 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:
|
||||||
|
|
||||||
- [wg-easy](https://github.com/wg-easy/wg-easy) as the server, providing a very simple web UI to manage connections and download config files (including QR codes for phones)
|
- [wg-easy](https://github.com/wg-easy/wg-easy) as the server, providing a very simple web UI to manage connections and download config files (including QR codes for phones)
|
||||||
- [WireGuard](https://docs.linuxserver.io/images/docker-wireguard/?h=wireguard) as the client for Linux systems
|
- [Wireguard](https://docs.linuxserver.io/images/docker-wireguard/?h=wireguard) as the client for Linux systems
|
||||||
|
|
||||||
Clients are also available for Windows, macOS, iOS, and Android.
|
Clients are also available for Windows, macOS, iOS, and Android.
|
||||||
|
|
||||||
@@ -32,7 +36,7 @@ To achieve this, we’ll create a **Virtual Private Network** (VPN), i.e., a sec
|
|||||||
|
|
||||||
Additionally, you can add your phone, laptop, or other devices to the VPN and securely access your server resources wherever you are.
|
Additionally, you can add your phone, laptop, or other devices to the VPN and securely access your server resources wherever you are.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
In this diagram, machine 1 is part of two networks:
|
In this diagram, machine 1 is part of two networks:
|
||||||
|
|
||||||
@@ -44,42 +48,38 @@ 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 Side
|
## Server Side
|
||||||
::note
|
---
|
||||||
|
::alert{type="info"}
|
||||||
📋 __Checklist:__
|
📋 __Checklist:__
|
||||||
|
|
||||||
- Ensure port `51820 UDP` is available and properly forwarded through your router to the server (`Source 51820 UDP -> Destination 51820 UDP -> Server`).
|
- Ensure port `51820 UDP` is available and properly forwarded through your router to the server (`Source 51820 UDP -> Destination 51820 UDP -> Server`).
|
||||||
- Ensure port `51821 TCP` is available for the web UI.
|
- Ensure port `51821 TCP` is available for the web UI.
|
||||||
::
|
::
|
||||||
|
|
||||||
::warning{to="https://wg-easy.github.io/wg-easy/latest/"}
|
::alert{type="warning"}
|
||||||
|
:::list{type="warning"}
|
||||||
__Warning:__ This guide uses version `14` of **wg-easy**. Version `15` introduces breaking changes incompatible with this configuration.
|
- __Warning:__ This guide uses version `14` of [wg-easy](https://wg-easy.github.io/wg-easy/latest/). Version `15` introduces breaking changes incompatible with this configuration.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
::file-tree
|
Folder structure:
|
||||||
---
|
|
||||||
tree:
|
```sh
|
||||||
/:
|
root
|
||||||
- srv:
|
└── docker
|
||||||
- docker:
|
└── wg-easy
|
||||||
- wg-easy:
|
├── config
|
||||||
- config:
|
│ └── etc_wireguard
|
||||||
- etc_wireguard/
|
├── compose.yaml
|
||||||
- compose.yaml
|
└── .env
|
||||||
- .env
|
```
|
||||||
---
|
|
||||||
::
|
|
||||||
|
|
||||||
The container runs in `HOST` mode, meaning it uses the host’s network stack directly.
|
The container runs in `HOST` mode, meaning it uses the host’s network stack directly.
|
||||||
|
|
||||||
::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`.
|
||||||
|
|
||||||
Paste the following configuration:
|
Paste the following configuration:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
---
|
||||||
services:
|
services:
|
||||||
wg-easy:
|
wg-easy:
|
||||||
@@ -99,22 +99,20 @@ services:
|
|||||||
image: ghcr.io/wg-easy/wg-easy:14
|
image: ghcr.io/wg-easy/wg-easy:14
|
||||||
container_name: wg-easy
|
container_name: wg-easy
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/wg_easy/config/etc_wireguard:/etc/wireguard
|
- /docker/wg_easy/config/etc_wireguard:/etc/wireguard
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
cap_add:
|
cap_add:
|
||||||
- NET_ADMIN
|
- NET_ADMIN
|
||||||
- SYS_MODULE
|
- SYS_MODULE
|
||||||
```
|
```
|
||||||
|
|
||||||
::tip{icon=""}
|
::alert{type="success"}
|
||||||
✨ __Tip:__
|
✨ __Tip:__
|
||||||
|
|
||||||
- You can also specify your own wireguard port with `WG_PORT`
|
- You can also specify your own wireguard port with `WG_PORT`
|
||||||
- Add the Watchtower label to enable automatic updates
|
- Add the Watchtower label to enable automatic updates
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
services
|
||||||
services:
|
|
||||||
wg-easy:
|
wg-easy:
|
||||||
#...
|
#...
|
||||||
labels:
|
labels:
|
||||||
@@ -124,7 +122,7 @@ services:
|
|||||||
|
|
||||||
In `.env`:
|
In `.env`:
|
||||||
|
|
||||||
```properties [.env]
|
```properties
|
||||||
HOST=
|
HOST=
|
||||||
PW=
|
PW=
|
||||||
ADDRESS=
|
ADDRESS=
|
||||||
@@ -140,14 +138,11 @@ IPS=
|
|||||||
|
|
||||||
Deploy the stack.
|
Deploy the stack.
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
|
||||||
|
|
||||||
### Enable Forwarding on Host
|
### Enable Forwarding on Host
|
||||||
|
|
||||||
To allow communication between VPN clients, enable:
|
To allow communication between VPN clients, enable:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo sysctl net.ipv4.ip_forward=1
|
sudo sysctl net.ipv4.ip_forward=1
|
||||||
sudo sysctl net.ipv4.conf.all.src_valid_mark=1
|
sudo sysctl net.ipv4.conf.all.src_valid_mark=1
|
||||||
```
|
```
|
||||||
@@ -156,72 +151,65 @@ sudo sysctl net.ipv4.conf.all.src_valid_mark=1
|
|||||||
|
|
||||||
To configure clients, download the config files from the server:
|
To configure clients, download the config files from the server:
|
||||||
|
|
||||||
- Visit `http://yourserverip:51821`
|
- Visit `http://your-server-ip:51821`
|
||||||
- Create a client
|
- Create a client
|
||||||
- Download the config file
|
- Download the config file
|
||||||
- Rename it to `wg0.conf`
|
- Rename it to `wg0.conf`
|
||||||
|
|
||||||
::caution
|
::alert{type="danger"}
|
||||||
|
:::list{type="danger"}
|
||||||
If it fails, check firewall rules.
|
- If it fails, check firewall rules.
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
## On the Client Server
|
## On the Client Server
|
||||||
::note
|
|
||||||
|
|
||||||
Assumes the client is a Linux server with Docker installed
|
|
||||||
::
|
|
||||||
|
|
||||||
::file-tree
|
|
||||||
---
|
|
||||||
tree:
|
|
||||||
/:
|
|
||||||
- srv:
|
|
||||||
- docker:
|
|
||||||
- wireguard:
|
|
||||||
- config:
|
|
||||||
- wg_confs/
|
|
||||||
- compose.yaml
|
|
||||||
---
|
---
|
||||||
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
|
- Assumes the client is a Linux server with Docker installed
|
||||||
|
:::
|
||||||
::
|
::
|
||||||
|
|
||||||
::steps{level="3"}
|
Folder structure:
|
||||||
### Create the config folder
|
|
||||||
|
|
||||||
Create the folder `/srv/docker/wireguard/config/wg_confs`:
|
```sh
|
||||||
|
root
|
||||||
::tip{icon="" to="/serveex/files/file-browser-quantum"}
|
└── docker
|
||||||
✨ __Tip:__ Use **File Browser** to browse and edit files without terminal
|
└── wireguard
|
||||||
::
|
└── config
|
||||||
|
│ └── wg_confs
|
||||||
```bash [Terminal]
|
└── compose.yaml
|
||||||
sudo mkdir -p /srv/docker/wireguard/config/wg_confs
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Copy the configuration file
|
Create the folder `/docker/wireguard/config/wg_confs`:
|
||||||
|
|
||||||
|
::alert{type="success"}
|
||||||
|
✨ __Tip:__ Use [File Browser](/serveex/files/file-browser) to browse and edit files without terminal
|
||||||
|
::
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo mkdir -p /docker/wireguard/config/wg_confs
|
||||||
|
```
|
||||||
|
|
||||||
Copy the `wg0.conf` file downloaded earlier:
|
Copy the `wg0.conf` file downloaded earlier:
|
||||||
|
|
||||||
::tip{icon=""}
|
::alert{type="success"}
|
||||||
✨ __Tip:__ Easiest way is to transfer the file via SFTP to `/home/youruser`, then move it:
|
✨ __Tip:__ Easiest way is to transfer the file via SFTP to `/home/youruser`, then move it:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
sudo cp ~/wg0.conf /srv/docker/wireguard/config/wg_confs
|
sudo cp ~/wg0.conf /docker/wireguard/config/wg_confs
|
||||||
```
|
```
|
||||||
::
|
::
|
||||||
|
|
||||||
### Deploy the container
|
Create `compose.yaml` in `/docker/wireguard`:
|
||||||
|
|
||||||
Create `compose.yaml` in `/srv/docker/wireguard`:
|
```sh
|
||||||
|
sudo vi /docker/wireguard/compose.yaml
|
||||||
```bash [Terminal]
|
|
||||||
sudo nano /srv/docker/wireguard/compose.yaml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Paste:
|
Press `i` to enter insert mode and paste:
|
||||||
|
|
||||||
```yaml [compose.yaml]
|
```yaml
|
||||||
---
|
|
||||||
services:
|
services:
|
||||||
wireguard:
|
wireguard:
|
||||||
image: lscr.io/linuxserver/wireguard:latest
|
image: lscr.io/linuxserver/wireguard:latest
|
||||||
@@ -233,39 +221,38 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- TZ=Europe/Paris
|
- TZ=Europe/Paris
|
||||||
volumes:
|
volumes:
|
||||||
- /srv/docker/wireguard/config:/config
|
- /docker/wireguard/config:/config
|
||||||
- /lib/modules:/lib/modules #optional
|
- /lib/modules:/lib/modules #optional
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
```
|
```
|
||||||
|
|
||||||
Press :kbd{value="Ctrl+O"}, then :kbd{value="Enter"} to save, and :kbd{value="Ctrl+X"} to exit.
|
Press `Esc` then type `:x` to save and exit.
|
||||||
|
|
||||||
Start the container:
|
Start the container:
|
||||||
|
|
||||||
```bash [Terminal]
|
```sh
|
||||||
cd /srv/docker/wireguard
|
cd /docker/wireguard
|
||||||
sudo docker compose up -d
|
sudo docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
::note
|
::alert{type="info"}
|
||||||
|
:::list{type="info"}
|
||||||
Repeat for each client
|
- Repeat for each client
|
||||||
::
|
:::
|
||||||
|
|
||||||
### Done!
|
|
||||||
::
|
::
|
||||||
|
|
||||||
## Other Devices
|
## Other Devices
|
||||||
|
---
|
||||||
|
- **Phone:** Install Wireguard and scan the QR code from the web UI (`http://your-server-ip:51821`)
|
||||||
|
- **PC:** Install the Wireguard client and import the config file
|
||||||
|
|
||||||
- **Phone:** Install WireGuard and scan the QR code from the web UI (`http://yourserverip:51821`)
|
::alert{type="warning"}
|
||||||
- **PC:** Install the WireGuard client and import the config file
|
:::list{type="warning"}
|
||||||
|
- __Warning:__ If a client device is on the same LAN as the server, edit `wg0.conf` and change the endpoint to the local server IP:
|
||||||
::warning
|
`Endpoint = your-server-ip:51820`
|
||||||
|
:::
|
||||||
__Warning:__ If a client device is on the same LAN as the server, edit `wg0.conf` and change the endpoint to the local server IP:
|
|
||||||
`Endpoint = yourserverip:51820`
|
|
||||||
::
|
::
|
||||||
|
|
||||||
And this is the result:
|
And this is the result:
|
||||||
|
|
||||||

|

|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
icon: lucide:trash-2
|
||||||
|
navigation.title: Deprecated
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
icon: noto:recycling-symbol
|
||||||
|
navigation.title: Recycled
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
title: About
|
|
||||||
icon: i-noto-star
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
---
|
|
||||||
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
|
|
||||||
::
|
|
||||||
::
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user