summaryrefslogtreecommitdiff
path: root/infra
diff options
context:
space:
mode:
Diffstat (limited to 'infra')
-rw-r--r--infra/Caddyfile110
-rw-r--r--infra/Caddyfile.local42
-rw-r--r--infra/chat/build-browser.sh34
-rw-r--r--infra/chat/build-native.sh63
-rwxr-xr-xinfra/chat/build-openssl-wasi.sh45
-rw-r--r--infra/chat/build-source.sh29
-rw-r--r--infra/chat/build-typescript.sh24
-rw-r--r--infra/chat/openssl-wasi.conf22
-rw-r--r--infra/containers/cgit/.containerignore21
-rw-r--r--infra/containers/cgit/Containerfile27
-rw-r--r--infra/containers/cgit/cgitrc27
-rw-r--r--infra/containers/cgit/entrypoint.sh20
-rw-r--r--infra/containers/cgit/nginx.conf46
-rw-r--r--infra/containers/cgit/supervisord.conf42
-rw-r--r--infra/containers/chat/Containerfile27
-rwxr-xr-xinfra/host-setup.sh28
-rw-r--r--infra/podman.conf17
-rw-r--r--infra/searxng/settings.yml24
-rw-r--r--infra/services.toml58
-rw-r--r--infra/stack.service34
20 files changed, 740 insertions, 0 deletions
diff --git a/infra/Caddyfile b/infra/Caddyfile
new file mode 100644
index 0000000..140101d
--- /dev/null
+++ b/infra/Caddyfile
@@ -0,0 +1,110 @@
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+{
+ http_port {{HTTP_PORT}}
+ https_port {{HTTPS_PORT}}
+}
+
+(https_security) {
+ header >Strict-Transport-Security "max-age=31536000"
+}
+
+https://{{BASE_DOMAIN}}:{{HTTPS_PORT}} {
+ import https_security
+ handle /.well-known/openpgpkey/* {
+ root * /srv
+ header Content-Type application/octet-stream
+ file_server
+ }
+
+ handle {
+ redir {{SITE_ORIGIN}}{uri} permanent
+ }
+}
+
+https://www.{{BASE_DOMAIN}}:{{HTTPS_PORT}} {
+ import https_security
+ redir {{SITE_ORIGIN}}{uri} permanent
+}
+
+https://{{SITE_HOST}}:{{HTTPS_PORT}} {
+ import https_security
+ redir /message {{CHAT_PUBLIC_ORIGIN}}/ permanent
+ redir /message/ {{CHAT_PUBLIC_ORIGIN}}/ permanent
+ root * /srv
+ file_server
+
+ redir /git /git/
+
+ handle /cgit* {
+ reverse_proxy {{CGIT_UPSTREAM}}
+ }
+
+ handle /git/* {
+ reverse_proxy {{CGIT_UPSTREAM}}
+ }
+}
+
+https://{{WKD_HOST}}:{{HTTPS_PORT}} {
+ import https_security
+ handle /.well-known/openpgpkey/* {
+ root * /srv
+ header Content-Type application/octet-stream
+ file_server
+ }
+}
+
+https://{{GIT_HOST}}:{{HTTPS_PORT}} {
+ import https_security
+ redir /{path}.git /{path} permanent
+
+ reverse_proxy {{CGIT_UPSTREAM}}
+}
+
+https://{{SEARCH_HOST}}:{{HTTPS_PORT}} {
+ import https_security
+ reverse_proxy {{SEARXNG_UPSTREAM}}
+}
+
+https://www.{{SITE_HOST}}:{{HTTPS_PORT}} {
+ import https_security
+ redir {{SITE_ORIGIN}}{uri} permanent
+}
+
+https://www.{{GIT_HOST}}:{{HTTPS_PORT}} {
+ import https_security
+ redir {{GIT_ORIGIN}}{uri} permanent
+}
+
+https://www.{{SEARCH_HOST}}:{{HTTPS_PORT}} {
+ import https_security
+ redir {{SEARCH_ORIGIN}}{uri} permanent
+}
+
+https://www.{{WKD_HOST}}:{{HTTPS_PORT}} {
+ import https_security
+ redir {{WKD_ORIGIN}}{uri} permanent
+}
+
+https://{{CHAT_HOST}}:{{HTTPS_PORT}} {
+ import https_security
+ reverse_proxy {{CHAT_UPSTREAM}}
+}
+
+https://www.{{CHAT_HOST}}:{{HTTPS_PORT}} {
+ import https_security
+ redir {{CHAT_PUBLIC_ORIGIN}}{uri} permanent
+}
diff --git a/infra/Caddyfile.local b/infra/Caddyfile.local
new file mode 100644
index 0000000..94114f9
--- /dev/null
+++ b/infra/Caddyfile.local
@@ -0,0 +1,42 @@
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+:80 {
+ redir /message {{CHAT_PUBLIC_ORIGIN}}/ permanent
+ redir /message/ {{CHAT_PUBLIC_ORIGIN}}/ permanent
+ redir /git /git/?{query} permanent
+ redir /search /search/?{query} permanent
+ @git path /git /git/* /cgit.js /cgit.png
+ handle @git {
+ uri strip_prefix /git
+ reverse_proxy {{CGIT_UPSTREAM}}
+ }
+
+ @search path /search /search/*
+ handle @search {
+ reverse_proxy {{SEARXNG_UPSTREAM}} {
+ header_up X-Script-Name /search
+ }
+ }
+
+ handle {
+ root * /srv
+ file_server
+ }
+}
+
+http://{{CHAT_LOCAL_HOST}}:80 {
+ reverse_proxy {{CHAT_UPSTREAM}}
+}
diff --git a/infra/chat/build-browser.sh b/infra/chat/build-browser.sh
new file mode 100644
index 0000000..dd0c777
--- /dev/null
+++ b/infra/chat/build-browser.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+set -eu
+ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
+SOURCE="$ROOT/.tools/wasi-source"
+SDK="$SOURCE/wasi-sdk-34.0-x86_64-linux"
+PREFIX="$ROOT/.tools/openssl-wasi"
+cd "$ROOT"
+test -x "$SDK/bin/clang"
+test -f "$PREFIX/lib/libcrypto.a"
+printf '%s %s\n' b761e3a0721dbae9c09a0059e5fdb2bf917d1b4a8a7b430fb3b5aafb0984b2c4 "$SOURCE/sdk.tar.gz" | sha256sum --check
+printf '%s %s\n' a2157c2830efdec3788939b00c9b0638306d3f0bbb76dc4832ee503bb397df98 "$SOURCE/openssl.tar.gz" | sha256sum --check
+rustup target add wasm32-wasip1
+RUST_LIB="$(rustc --print target-libdir --target wasm32-wasip1)/self-contained"
+env LIBCLANG_PATH="$SDK/lib" OPENSSL_DIR="$PREFIX" OPENSSL_STATIC=1 \
+ CC_wasm32_wasip1="$SDK/bin/clang" AR_wasm32_wasip1="$SDK/bin/llvm-ar" \
+ BINDGEN_EXTRA_CLANG_ARGS_wasm32_wasip1="--sysroot=$SDK/share/wasi-sysroot --target=wasm32-wasip1 -fvisibility=default" \
+ RUSTFLAGS="-Lnative=$RUST_LIB -Lnative=$SDK/share/wasi-sysroot/lib/wasm32-wasip1 -Clink-arg=-lwasi-emulated-getpid -Clink-arg=--max-memory=268435456" \
+ cargo build --locked --release -p stack-chat-crypto --bin stack-chat-crypto --target wasm32-wasip1
+sha256sum target/wasm32-wasip1/release/stack-chat-crypto.wasm
diff --git a/infra/chat/build-native.sh b/infra/chat/build-native.sh
new file mode 100644
index 0000000..f882fef
--- /dev/null
+++ b/infra/chat/build-native.sh
@@ -0,0 +1,63 @@
+#!/bin/sh
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+set -eu
+ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
+TOOLS="$ROOT/.tools"
+SOURCE="$TOOLS/openssl-native-source"
+PREFIX="$TOOLS/openssl-musl"
+HEADERS="$TOOLS/alpine/usr/include"
+KERNEL="$TOOLS/kernel-headers"
+mkdir -p "$SOURCE" "$TOOLS/wasi-source" "$TOOLS/alpine" "$KERNEL"
+fetch() {
+ file="$1"
+ digest="$2"
+ url="$3"
+ if ! test -f "$file"; then
+ curl --fail --location --retry 2 "$url" --output "$file.download"
+ printf '%s %s\n' "$digest" "$file.download" | sha256sum --check --status
+ mv "$file.download" "$file"
+ fi
+ printf '%s %s\n' "$digest" "$file" | sha256sum --check
+}
+fetch "$TOOLS/wasi-source/openssl.tar.gz" a2157c2830efdec3788939b00c9b0638306d3f0bbb76dc4832ee503bb397df98 https://github.com/openssl/openssl/releases/download/openssl-3.6.5/openssl-3.6.5.tar.gz
+fetch "$TOOLS/musl-dev.apk" 310b51cff12e1d5ce5f135c558a82e2550427da5465bbd16c9ff9b05e63ee3d9 https://dl-cdn.alpinelinux.org/alpine/v3.24/main/x86_64/musl-dev-1.2.6-r3.apk
+if ! test -f "$HEADERS/stdio.h"; then tar -xzf "$TOOLS/musl-dev.apk" -C "$TOOLS/alpine" usr/include; fi
+if ! test -f "$SOURCE/openssl-3.6.5/Configure"; then tar -xzf "$TOOLS/wasi-source/openssl.tar.gz" -C "$SOURCE"; fi
+GCC_INCLUDE="$(cc -print-file-name=include)"
+KERNEL_ASM=/usr/include/asm
+if ! test -d "$KERNEL_ASM"; then KERNEL_ASM="/usr/include/$(cc -dumpmachine)/asm"; fi
+test -f /usr/include/linux/mman.h
+test -d "$KERNEL_ASM"
+ln -sfn /usr/include/linux "$KERNEL/linux"
+ln -sfn "$KERNEL_ASM" "$KERNEL/asm"
+ln -sfn /usr/include/asm-generic "$KERNEL/asm-generic"
+MUSL_CC="cc -nostdinc -isystem $HEADERS -isystem $GCC_INCLUDE -idirafter $KERNEL"
+STAMP="$(sha256sum "$ROOT/infra/chat/build-native.sh" | cut -d' ' -f1)"
+if ! test -f "$PREFIX/.stack-build" || ! test "$(cat "$PREFIX/.stack-build")" = "$STAMP"; then
+ cd "$SOURCE/openssl-3.6.5"
+ env CC="$MUSL_CC" ./Configure linux-x86_64 no-shared no-module no-dso no-tests no-autoload-config --with-rand-seed=getrandom --prefix="$PREFIX" --libdir=lib
+ make -j"${CHAT_BUILD_JOBS:-8}" build_libs
+ make install_dev
+ printf '%s\n' "$STAMP" > "$PREFIX/.stack-build"
+fi
+cd "$ROOT"
+rustup target add x86_64-unknown-linux-musl
+env OPENSSL_DIR="$PREFIX" OPENSSL_STATIC=1 CC_x86_64_unknown_linux_musl="$MUSL_CC" \
+ BINDGEN_EXTRA_CLANG_ARGS_x86_64_unknown_linux_musl="-nostdinc -isystem $HEADERS -isystem $GCC_INCLUDE -idirafter $KERNEL" \
+ cargo build --locked --release --target x86_64-unknown-linux-musl -p stack -p stack-chat-server -p stack-chat-crypto
+"$ROOT/target/x86_64-unknown-linux-musl/release/stack-chat-crypto"
+file "$ROOT/target/x86_64-unknown-linux-musl/release/stack-chat-server"
diff --git a/infra/chat/build-openssl-wasi.sh b/infra/chat/build-openssl-wasi.sh
new file mode 100755
index 0000000..91c0583
--- /dev/null
+++ b/infra/chat/build-openssl-wasi.sh
@@ -0,0 +1,45 @@
+#!/bin/sh
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+set -eu
+ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
+SOURCE="$ROOT/.tools/wasi-source"
+SDK="$SOURCE/wasi-sdk-34.0-x86_64-linux"
+PREFIX="$ROOT/.tools/openssl-wasi"
+mkdir -p "$SOURCE"
+
+fetch() {
+ file="$1"
+ digest="$2"
+ url="$3"
+ if ! test -f "$file"; then
+ curl --fail --location --retry 2 "$url" --output "$file.download"
+ printf '%s %s\n' "$digest" "$file.download" | sha256sum --check --status
+ mv "$file.download" "$file"
+ fi
+ printf '%s %s\n' "$digest" "$file" | sha256sum --check
+}
+
+fetch "$SOURCE/sdk.tar.gz" b761e3a0721dbae9c09a0059e5fdb2bf917d1b4a8a7b430fb3b5aafb0984b2c4 https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-34/wasi-sdk-34.0-x86_64-linux.tar.gz
+fetch "$SOURCE/openssl.tar.gz" a2157c2830efdec3788939b00c9b0638306d3f0bbb76dc4832ee503bb397df98 https://github.com/openssl/openssl/releases/download/openssl-3.6.5/openssl-3.6.5.tar.gz
+if ! test -x "$SDK/bin/clang"; then tar -xzf "$SOURCE/sdk.tar.gz" -C "$SOURCE"; fi
+if ! test -f "$SOURCE/openssl-3.6.5/Configure"; then tar -xzf "$SOURCE/openssl.tar.gz" -C "$SOURCE"; fi
+cp "$ROOT/infra/chat/openssl-wasi.conf" "$SOURCE/openssl-3.6.5/Configurations/99-stack-wasi.conf"
+cd "$SOURCE/openssl-3.6.5"
+env CC="$SDK/bin/clang" AR="$SDK/bin/llvm-ar" RANLIB="$SDK/bin/llvm-ranlib" CFLAGS='-O2 -D_WASI_EMULATED_GETPID -Dgetuid=getpagesize -Dgeteuid=getpagesize -Dgetgid=getpagesize -Dgetegid=getpagesize' LDFLAGS='-lwasi-emulated-getpid' ./Configure stack-wasi --prefix="$PREFIX" --libdir=lib --with-rand-seed=getrandom no-asm no-async no-autoload-config no-dso no-module no-posix-io no-secure-memory no-shared no-sock no-stdio no-thread-pool no-threads no-ui-console
+make -j"${CHAT_BUILD_JOBS:-8}" build_libs
+make install_dev
+sh "$ROOT/infra/chat/build-browser.sh"
diff --git a/infra/chat/build-source.sh b/infra/chat/build-source.sh
new file mode 100644
index 0000000..475b519
--- /dev/null
+++ b/infra/chat/build-source.sh
@@ -0,0 +1,29 @@
+#!/bin/sh
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+set -eu
+ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
+cd "$ROOT"
+BUILD="$ROOT/var/build"
+mkdir -p "$BUILD"
+cargo vendor --locked var/build/chat-vendor | sed 's|directory = "var/build/chat-vendor"|directory = "chat-vendor"|' > "$BUILD/chat-vendor-config.txt"
+cp "$ROOT/.tools/wasi-source/openssl.tar.gz" "$BUILD/chat-openssl-3.6.5.tar.gz"
+printf '%s %s\n' a2157c2830efdec3788939b00c9b0638306d3f0bbb76dc4832ee503bb397df98 "$BUILD/chat-openssl-3.6.5.tar.gz" | sha256sum --check
+source_list="$BUILD/chat-source-files.txt"
+find clippy.toml eslint.config.ts package.json package-lock.json tsconfig.json tsconfig.browser.json tsconfig.worker.json tsconfig.tests.json tsconfig.tools.json tests Cargo.toml Cargo.lock rust-toolchain.toml .cargo .gitignore .gitattributes src crates infra web docs README.txt LICENSE deny.toml about.toml about.hbs .env.example -type f -print > "$source_list"
+tar -czf "$BUILD/chat-source.tar.gz.pending" --verbatim-files-from -T "$source_list" -C "$BUILD" chat-vendor chat-vendor-config.txt chat-openssl-3.6.5.tar.gz
+mv "$BUILD/chat-source.tar.gz.pending" "$BUILD/chat-source.tar.gz"
+sha256sum "$BUILD/chat-source.tar.gz"
diff --git a/infra/chat/build-typescript.sh b/infra/chat/build-typescript.sh
new file mode 100644
index 0000000..14cae95
--- /dev/null
+++ b/infra/chat/build-typescript.sh
@@ -0,0 +1,24 @@
+#!/bin/sh
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+set -eu
+ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
+cd "$ROOT"
+npm ci --ignore-scripts --no-audit --no-fund --cache "$ROOT/.tools/npm-cache"
+npm run check
+node node_modules/typescript-native/bin/tsc -p tsconfig.browser.json
+node node_modules/typescript-native/bin/tsc -p tsconfig.worker.json
+node node_modules/typescript-native/bin/tsc -p tsconfig.tests.json
diff --git a/infra/chat/openssl-wasi.conf b/infra/chat/openssl-wasi.conf
new file mode 100644
index 0000000..479adee
--- /dev/null
+++ b/infra/chat/openssl-wasi.conf
@@ -0,0 +1,22 @@
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+my %targets = (
+ "stack-wasi" => {
+ inherit_from => [ "BASE_unix" ],
+ lib_cppflags => add("-DL_ENDIAN"),
+ bn_ops => "THIRTY_TWO_BIT",
+ },
+);
diff --git a/infra/containers/cgit/.containerignore b/infra/containers/cgit/.containerignore
new file mode 100644
index 0000000..4fb13e4
--- /dev/null
+++ b/infra/containers/cgit/.containerignore
@@ -0,0 +1,21 @@
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+*
+!Containerfile
+!entrypoint.sh
+!nginx.conf
+!supervisord.conf
+!cgitrc
diff --git a/infra/containers/cgit/Containerfile b/infra/containers/cgit/Containerfile
new file mode 100644
index 0000000..3bd130a
--- /dev/null
+++ b/infra/containers/cgit/Containerfile
@@ -0,0 +1,27 @@
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+FROM docker.io/library/debian:trixie-slim@sha256:1e1a22175fbf1b32157ddaf0219e1db4e5401fab2b94ed285add64d18587cd2d
+RUN apt-get update \
+ && apt-get install -y --no-install-recommends \
+ cgit fcgiwrap nginx-light supervisor curl ca-certificates git \
+ && rm -rf /var/lib/apt/lists/* /etc/nginx/sites-enabled/default
+COPY nginx.conf /etc/nginx/nginx.conf
+COPY supervisord.conf /etc/supervisor/stack.conf
+COPY cgitrc /etc/cgitrc
+COPY entrypoint.sh /usr/local/bin/stack-cgit
+RUN chmod 755 /usr/local/bin/stack-cgit
+EXPOSE 8080
+ENTRYPOINT ["/usr/local/bin/stack-cgit"]
diff --git a/infra/containers/cgit/cgitrc b/infra/containers/cgit/cgitrc
new file mode 100644
index 0000000..fb9cae8
--- /dev/null
+++ b/infra/containers/cgit/cgitrc
@@ -0,0 +1,27 @@
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+root-title=Doan Luu Duc Tai's repositories
+root-desc=Personal git repos
+virtual-root=/
+clone-prefix={{GIT_ORIGIN}}
+css=/cgit.css
+favicon=/favicon.ico
+remove-suffix=1
+defbranch=main
+cache-root=/var/cache/cgit
+cache-size=1000
+cache-scanrc-ttl=1
+scan-path=/srv/git
diff --git a/infra/containers/cgit/entrypoint.sh b/infra/containers/cgit/entrypoint.sh
new file mode 100644
index 0000000..5de3a06
--- /dev/null
+++ b/infra/containers/cgit/entrypoint.sh
@@ -0,0 +1,20 @@
+#!/bin/sh
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+set -eu
+mkdir -p /run/fcgiwrap /var/cache/cgit
+chown www-data:www-data /run/fcgiwrap /var/cache/cgit
+exec /usr/bin/supervisord -n -c /etc/supervisor/stack.conf
diff --git a/infra/containers/cgit/nginx.conf b/infra/containers/cgit/nginx.conf
new file mode 100644
index 0000000..df50345
--- /dev/null
+++ b/infra/containers/cgit/nginx.conf
@@ -0,0 +1,46 @@
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+user www-data;
+worker_processes auto;
+pid /run/nginx.pid;
+error_log /dev/stderr warn;
+events { worker_connections 1024; }
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+ access_log /dev/stdout;
+ map $http_x_forwarded_proto $cgi_https {
+ default "";
+ https on;
+ }
+ server {
+ listen 8080;
+ server_name _;
+ root /usr/share/cgit;
+ location ~* ^/(cgit\.(css|png|js)|favicon\.ico|robots\.txt)$ {
+ expires 30d;
+ }
+ location / {
+ include /etc/nginx/fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME /usr/lib/cgit/cgit.cgi;
+ fastcgi_param PATH_INFO $uri;
+ fastcgi_param QUERY_STRING $args;
+ fastcgi_param HTTP_HOST $http_host;
+ fastcgi_param HTTPS $cgi_https;
+ fastcgi_pass unix:/run/fcgiwrap/socket;
+ }
+ }
+}
diff --git a/infra/containers/cgit/supervisord.conf b/infra/containers/cgit/supervisord.conf
new file mode 100644
index 0000000..1a93bb5
--- /dev/null
+++ b/infra/containers/cgit/supervisord.conf
@@ -0,0 +1,42 @@
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+[supervisord]
+nodaemon=true
+user=root
+logfile=/dev/null
+pidfile=/run/supervisord.pid
+
+[program:fcgiwrap]
+command=/usr/sbin/fcgiwrap -f -s unix:/run/fcgiwrap/socket
+user=www-data
+umask=0007
+autorestart=true
+stopasgroup=true
+killasgroup=true
+stdout_logfile=/dev/fd/1
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/fd/2
+stderr_logfile_maxbytes=0
+
+[program:nginx]
+command=/usr/sbin/nginx -g "daemon off;"
+autorestart=true
+stopasgroup=true
+killasgroup=true
+stdout_logfile=/dev/fd/1
+stdout_logfile_maxbytes=0
+stderr_logfile=/dev/fd/2
+stderr_logfile_maxbytes=0
diff --git a/infra/containers/chat/Containerfile b/infra/containers/chat/Containerfile
new file mode 100644
index 0000000..31724b5
--- /dev/null
+++ b/infra/containers/chat/Containerfile
@@ -0,0 +1,27 @@
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+FROM scratch
+COPY server /app/bin/server
+COPY crypto.wasm /app/bin/chat/crypto.wasm
+COPY SOURCE.tar.gz /app/bin/chat/SOURCE.tar.gz
+COPY web /app/bin/chat/web
+COPY wasi.mjs /app/bin/chat/wasi.mjs
+COPY CHAT-APPLICATION.txt /app/docs/CHAT-APPLICATION.txt
+COPY CHAT-PRODUCTION.txt /app/docs/CHAT-PRODUCTION.txt
+COPY LICENSE /app/LICENSE
+WORKDIR /app
+EXPOSE 8080
+ENTRYPOINT ["/app/bin/server", "--root", "/app", "--proxy"]
diff --git a/infra/host-setup.sh b/infra/host-setup.sh
new file mode 100755
index 0000000..e73645b
--- /dev/null
+++ b/infra/host-setup.sh
@@ -0,0 +1,28 @@
+#!/bin/sh
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+set -eu
+[ "$(id -u)" = 0 ] || { echo 'Run this script through sudo.' >&2; exit 1; }
+id ops >/dev/null
+apt-get update
+apt-get install -y --no-install-recommends podman uidmap passt slirp4netns fuse-overlayfs netavark aardvark-dns
+awk -F: '$1 == "ops" && $3 >= 65536 { found=1 } END { exit !found }' /etc/subuid
+awk -F: '$1 == "ops" && $3 >= 65536 { found=1 } END { exit !found }' /etc/subgid
+install -d -m 755 /etc/sysctl.d
+printf '%s\n' 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/90-stack-rootless-ports.conf
+sysctl -p /etc/sysctl.d/90-stack-rootless-ports.conf
+loginctl enable-linger ops
+printf '%s\n' 'Podman prerequisites ready. Existing production services remain running.'
diff --git a/infra/podman.conf b/infra/podman.conf
new file mode 100644
index 0000000..5e6965d
--- /dev/null
+++ b/infra/podman.conf
@@ -0,0 +1,17 @@
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+[network]
+default_rootless_network_cmd = "slirp4netns"
diff --git a/infra/searxng/settings.yml b/infra/searxng/settings.yml
new file mode 100644
index 0000000..f8f7d6b
--- /dev/null
+++ b/infra/searxng/settings.yml
@@ -0,0 +1,24 @@
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+use_default_settings: true
+
+server:
+ limiter: true
+ public_instance: false
+
+search:
+ formats:
+ - html
diff --git a/infra/services.toml b/infra/services.toml
new file mode 100644
index 0000000..7fea853
--- /dev/null
+++ b/infra/services.toml
@@ -0,0 +1,58 @@
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+format = 1
+
+[services.caddy]
+image = "docker.io/library/caddy:2-alpine@sha256:5f5c8640aae01df9654968d946d8f1a56c497f1dd5c5cda4cf95ab7c14d58648"
+gateway = true
+health = ["caddy", "validate", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"]
+mounts = [
+ { source = "@caddy", target = "/etc/caddy/Caddyfile", read_only = true },
+ { source = "var/data/caddy/data", target = "/data" },
+ { source = "var/data/caddy/config", target = "/config" },
+ { source = "@web", target = "/srv", read_only = true },
+]
+
+[services.cgit]
+image = "localhost/stack-cgit:0.1.0"
+build = "infra/containers/cgit"
+health = ["curl", "--fail", "--silent", "--max-time", "3", "http://127.0.0.1:8080/"]
+mounts = [
+ { source = "var/data/repos", target = "/srv/git", read_only = true },
+ { source = "@cgit", target = "/etc/cgitrc", read_only = true },
+]
+tmpfs = ["/run", "/var/cache/cgit"]
+
+[services.searxng]
+image = "docker.io/searxng/searxng:latest@sha256:1dab138ea70a8ceb4d1c182f5c8f256088406c1742b99e279976157dbe1e9759"
+environment = ["SEARXNG_SECRET"]
+drop_capabilities = true
+health = ["wget", "--quiet", "--spider", "http://127.0.0.1:8080/"]
+mounts = [
+ { source = "var/data/searxng", target = "/etc/searxng" },
+]
+tmpfs = ["/var/cache/searxng:mode=1777"]
+
+[services.chat]
+image = "localhost/stack-chat:0.1.0"
+build = "bin/chat"
+drop_capabilities = true
+read_only = true
+health = ["/app/bin/server", "--root", "/app", "--proxy", "--healthcheck"]
+environment = ["CHAT_SETUP_TOKEN"]
+mounts = [
+ { source = "@chat", target = "/app/var/data/chat" },
+]
diff --git a/infra/stack.service b/infra/stack.service
new file mode 100644
index 0000000..702da97
--- /dev/null
+++ b/infra/stack.service
@@ -0,0 +1,34 @@
+# Copyright (C) 2026 Doan Luu Duc Tai <doan@luuduct.ai>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+[Unit]
+Description=Personal web stack
+Wants=network-online.target
+After=network-online.target
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+KillMode=process
+WorkingDirectory=%h/stack
+ExecStart=%h/stack/bin/stack up --production
+ExecStop=%h/stack/bin/stack down
+TimeoutStartSec=300
+TimeoutStopSec=120
+Restart=on-failure
+RestartSec=10
+
+[Install]
+WantedBy=default.target