aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorThomas Vanbesien <tvanbesi@proton.me>2026-02-11 21:00:52 +0100
committerThomas Vanbesien <tvanbesi@proton.me>2026-02-12 14:56:15 +0100
commit6c22a6e48e8ff49a69434eca7a7b78158576cb7b (patch)
tree2648d665bf9c27166da052fe2aa8281b56ceb498
downloadnet_services-6c22a6e48e8ff49a69434eca7a7b78158576cb7b.tar.gz
net_services-6c22a6e48e8ff49a69434eca7a7b78158576cb7b.zip
Initial import
-rw-r--r--.gitignore2
-rwxr-xr-xbuild.command5
-rw-r--r--compose.yaml72
-rwxr-xr-xcreate_radicale_user.command4
-rw-r--r--example.env10
-rwxr-xr-xgenerate_self_signed_cert.bash7
-rw-r--r--readme.md39
-rw-r--r--services/cgit/Dockerfile40
-rw-r--r--services/cgit/cgit.conf1
-rw-r--r--services/cgit/httpd.conf63
-rw-r--r--services/cgit/readme.md73
-rw-r--r--services/nginx/Dockerfile3
-rw-r--r--services/nginx/fs/etc/nginx/templates/default.conf.template45
-rw-r--r--services/nginx/fs/etc/nginx/templates/services/cgit.conf.template17
-rw-r--r--services/nginx/fs/etc/nginx/templates/services/radicale.conf.template19
-rwxr-xr-xservices/nginx/fs/sbin/cmd.bash11
-rw-r--r--services/radicale/Dockerfile3
-rw-r--r--services/radicale/fs/etc/radicale/conf.ini14
-rwxr-xr-xservices/radicale/fs/sbin/cmd.sh33
19 files changed, 461 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..41a9d3b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/.env
+**/fs.tar.gz
diff --git a/build.command b/build.command
new file mode 100755
index 0000000..d307f88
--- /dev/null
+++ b/build.command
@@ -0,0 +1,5 @@
+#!/usr/bin/bash
+
+for srv in nginx radicale; do
+ tar -czf services/"$srv"/fs.tar.gz -C services/"$srv"/fs .
+done
diff --git a/compose.yaml b/compose.yaml
new file mode 100644
index 0000000..9b148eb
--- /dev/null
+++ b/compose.yaml
@@ -0,0 +1,72 @@
+name: net_services
+
+services:
+ nginx:
+ image: nginx:${COMPOSE_PROJECT_NAME}
+ build:
+ context: services/nginx
+ environment:
+ - NGINX__HOST=${NGINX__HOST}
+ networks:
+ - cgit
+ - radicale
+ ports:
+ - ${HOST__HTTP_PORT:?}:80
+ - ${HOST__HTTPS_PORT:?}:443
+ tmpfs:
+ - /run/secrets:mode=400
+ volumes:
+ - ${HOST__CERT_DIR:?}:/run/host_secrets:ro
+ depends_on:
+ - cgit
+ - radicale
+
+ cgit:
+ image: cgit:${COMPOSE_PROJECT_NAME}
+ build:
+ context: services/cgit
+ networks:
+ - cgit
+ volumes:
+ - ${HOST__CGITRC_DIR:?}:/etc/cgit:ro
+ - ${HOST__CGIT_FILTER_DIR:?}:/usr/local/lib/cgit/filters/commit
+ - ${HOST__CGIT_ABOUT_DIR:?}:/srv/cgit:ro
+ - ${HOST__GIT_REPO_DIR:?}:/srv/git:ro
+
+ radicale:
+ image: tomsquest/docker-radicale:tvcloud
+ build:
+ context: services/radicale
+ environment:
+ - TAKE_FILE_OWNERSHIP=false
+ init: true
+ read_only: true
+ security_opt:
+ - no-new-privileges:true
+ cap_drop:
+ - ALL
+ cap_add:
+ - SETUID
+ - SETGID
+ - CHOWN
+ - KILL
+ deploy:
+ resources:
+ limits:
+ memory: 256M
+ pids: 50
+ healthcheck:
+ test: curl -f http://127.0.0.1:5232 || exit 1
+ start_period: 5s
+ networks:
+ - radicale
+ volumes:
+ - ${HOST__RADICALE_USERS_DIR:?}:/etc/radicale/users:ro
+ - radicale_data:/data
+
+networks:
+ cgit:
+ radicale:
+
+volumes:
+ radicale_data:
diff --git a/create_radicale_user.command b/create_radicale_user.command
new file mode 100755
index 0000000..0b072e6
--- /dev/null
+++ b/create_radicale_user.command
@@ -0,0 +1,4 @@
+#!/usr/bin/bash
+
+username=${1:?missing argument username}
+htpasswd -nBC 12 "$username"
diff --git a/example.env b/example.env
new file mode 100644
index 0000000..6bf613f
--- /dev/null
+++ b/example.env
@@ -0,0 +1,10 @@
+HOST__HTTP_PORT=80
+HOST__HTTPS_PORT=443
+HOST__CERT_DIR=/home/USER/.local/net_services/certs
+HOST__GIT_REPO_DIR=/home/USER/.local/net_services/git
+HOST__CGITRC_DIR=/home/USER/.local/net_services/cgit/cgitrc
+HOST__CGIT_FILTER_DIR=/home/USER/.local/net_services/cgit/filter
+HOST__CGIT_ABOUT_DIR=/home/USER/.local/net_services/cgit/about
+HOST__RADICALE_USERS_DIR=/home/USER/.local/net_services/radicale
+
+NGINX__HOST=localhost
diff --git a/generate_self_signed_cert.bash b/generate_self_signed_cert.bash
new file mode 100755
index 0000000..379ea13
--- /dev/null
+++ b/generate_self_signed_cert.bash
@@ -0,0 +1,7 @@
+#!/usr/bin/bash
+
+host=${1:?missing host argument}
+subdomains=(www git dav)
+
+mkcert -install
+mkcert "${subdomains[@]/%/.$host}" "$host"
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..0e199b6
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,39 @@
+# net_services
+
+This is personal project about services on the network I provide for myself.
+
+I want to handle as much of my data myself. For privacy and for the challenge. I also want to serve apps for my friends.
+
+I set this up on a VPS with a static IP. I also bought a domain name `tvcloud.fr` to point to the VPS. Some services run on the VPS itself. Some others are run with Docker. Nginx is used as endpoint.
+
+## How-to
+
+1. Create a `.env`. See `example.env`.
+
+1. Build and run the services.
+
+ ```
+ ./build.command
+ docker compose up
+ ```
+
+## Handling data
+
+Data of various types has to be handled in different ways.
+
+* **Passwords**: A KeePassXC database shared with Syncthing.
+
+* **Git repositories**: A remote server accesible over SSH for push. And also a web front-end (cgit).
+
+* **Calendars, to-dos, journals, and contacts**: A Radicale server.
+ I could just synchronize the `.ics`/`.vcf` files, but a CalDAV/CarDAV server is compatible with mobile applications.
+
+* **Remote storage**: SFTP for large files. Syncthing for moderately large data that is better synchronized than downloaded manually.
+
+## Security
+
+TODO (sensitive data in tmpfs)
+
+### Firewalls
+
+TODO (OVH, iptables, docker+iptables+reboot bug)
diff --git a/services/cgit/Dockerfile b/services/cgit/Dockerfile
new file mode 100644
index 0000000..4c23eb2
--- /dev/null
+++ b/services/cgit/Dockerfile
@@ -0,0 +1,40 @@
+FROM debian:13.3-slim AS build
+
+ARG CGIT_COMMIT=09d24d7cd0b7e85633f2f43808b12871bb209d69
+
+# Install build dependencies
+RUN apt-get update \
+ && apt-get install --assume-yes --no-install-recommends \
+ make gcc pkg-config curl xz-utils ca-certificates libzip-dev libssl-dev liblua5.2-dev \
+ && rm -rf /var/lib/apt/lists/*
+
+# Build cgit
+ADD --unpack=true https://git.zx2c4.com/cgit/snapshot/cgit-${CGIT_COMMIT}.tar.xz /usr/src
+WORKDIR /usr/src/cgit-${CGIT_COMMIT}
+COPY cgit.conf .
+RUN make get-git && make LUA_PKGCONFIG=lua5.2 && make install && rm -rf $(pwd)
+
+FROM httpd:2.4.66 AS final
+
+ARG UID=1000 GID=1000
+
+# Create cgit user (used by Apache)
+RUN groupadd --gid ${GID} cgit && useradd --uid ${UID} --groups cgit --no-user-group cgit
+
+# Copy cgit built in previous stage
+COPY --from=build /var/www/htdocs/cgit /var/www/htdocs
+COPY --from=build /usr/local/lib/cgit/filters /usr/local/lib/cgit/filters
+RUN mkdir /var/cache/cgit && chown cgit:cgit /var/cache/cgit
+
+# Install runtime dependencies
+RUN apt-get update \
+ && apt-get install --assume-yes --no-install-recommends \
+ python3 python3-pygments python3-markdown \
+ && rm -rf /var/lib/apt/lists/*
+
+# HTTP server configuration
+COPY httpd.conf /usr/local/apache2/conf/
+
+WORKDIR /var/www/htdocs
+EXPOSE 80
+VOLUME /srv/git /etc/cgit /usr/local/lib/cgit/filters/commit /srv/cgit
diff --git a/services/cgit/cgit.conf b/services/cgit/cgit.conf
new file mode 100644
index 0000000..446a846
--- /dev/null
+++ b/services/cgit/cgit.conf
@@ -0,0 +1 @@
+CGIT_CONFIG = /etc/cgit/cgitrc
diff --git a/services/cgit/httpd.conf b/services/cgit/httpd.conf
new file mode 100644
index 0000000..451603c
--- /dev/null
+++ b/services/cgit/httpd.conf
@@ -0,0 +1,63 @@
+#
+# Apache HTTP server configuration
+#
+
+LoadModule rewrite_module modules/mod_rewrite.so
+LoadModule mpm_event_module modules/mod_mpm_event.so
+LoadModule authn_file_module modules/mod_authn_file.so
+LoadModule authn_core_module modules/mod_authn_core.so
+LoadModule authz_host_module modules/mod_authz_host.so
+LoadModule authz_groupfile_module modules/mod_authz_groupfile.so
+LoadModule authz_user_module modules/mod_authz_user.so
+LoadModule authz_core_module modules/mod_authz_core.so
+LoadModule access_compat_module modules/mod_access_compat.so
+LoadModule auth_basic_module modules/mod_auth_basic.so
+LoadModule reqtimeout_module modules/mod_reqtimeout.so
+LoadModule filter_module modules/mod_filter.so
+LoadModule mime_module modules/mod_mime.so
+LoadModule log_config_module modules/mod_log_config.so
+LoadModule env_module modules/mod_env.so
+LoadModule headers_module modules/mod_headers.so
+LoadModule setenvif_module modules/mod_setenvif.so
+LoadModule version_module modules/mod_version.so
+LoadModule unixd_module modules/mod_unixd.so
+LoadModule status_module modules/mod_status.so
+LoadModule autoindex_module modules/mod_autoindex.so
+<IfModule !mpm_prefork_module>
+ LoadModule cgid_module modules/mod_cgid.so
+</IfModule>
+<IfModule mpm_prefork_module>
+ LoadModule cgi_module modules/mod_cgi.so
+</IfModule>
+LoadModule dir_module modules/mod_dir.so
+LoadModule alias_module modules/mod_alias.so
+
+ServerName localhost
+ServerRoot "/usr/local/apache2"
+Listen 80
+User cgit
+Group cgit
+
+DocumentRoot "/var/www/htdocs"
+<Directory "/var/www/htdocs">
+ Options +ExecCGI
+ AddHandler cgi-script .cgi
+ RewriteEngine on
+ # Serve regular files
+ RewriteCond %{REQUEST_FILENAME} -f
+ RewriteRule ^ - [L]
+ # URLs not starting with "cgit.cgi" are internally prefixed with it
+ RewriteRule "^(?!cgit\.cgi)(.*)" "/cgit.cgi/$1" [L]
+</Directory>
+
+<Files ".ht*">
+ Require all denied
+</Files>
+
+ErrorLog /proc/self/fd/2
+LogLevel warn
+# Uncomment to see rewrite module trace
+# LogLevel info rewrite_module:trace1
+LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined
+LogFormat "%h %l %u %t \"%r\" %>s %b" common
+CustomLog /proc/self/fd/1 common
diff --git a/services/cgit/readme.md b/services/cgit/readme.md
new file mode 100644
index 0000000..bef4b9b
--- /dev/null
+++ b/services/cgit/readme.md
@@ -0,0 +1,73 @@
+# cgit
+
+This project is a [cgit](https://git.zx2c4.com/cgit/about/) docker image.
+
+It aims to be as simple as possible. No authentication, no SSH, just browsing repositories on a web page. The container doesn't write to the repositories so they can be read-only.
+
+# Build
+
+```
+docker build --tag cgit .
+```
+
+# Run
+
+## cgit configuration and runtime
+
+Examples are provided in the `examples` directory.
+
+* `CGITRC`: Host directory containing a `cgitrc` configuration, see [cgitrc manual](https://manpages.debian.org/trixie/cgit/cgitrc.5.en.html).
+* `COMMIT_FILTER`: Host directory containing an executable `commit-filter.sh` script to format Git commit messages. See the `commit-filter` section of the [cgitrc manual](https://manpages.debian.org/trixie/cgit/cgitrc.5.en.html).
+* `ABOUT`: Host directory containing `about.md` for the front page "about" section.
+* `REPOSITORIES`: Host directory containing your Git repositories.
+
+## Run with `docker`
+
+```
+docker run \
+ --rm \
+ --name cgit \
+ --publish 8080:80 \
+ --mount type=bind,src=CGITRC,dst=/etc/cgit,ro \
+ --mount type=bind,src=COMMIT_FILTER,dst=/usr/local/lib/cgit/filters/commit \
+ --mount type=bind,src=ABOUT,dst=/srv/cgit,ro \
+ --mount type=bind,src=REPOSITORIES,dst=/srv/git,ro \
+ cgit
+```
+
+Browse the website [here](http://localhost:8080).
+
+## Run with `docker compose`
+
+*Example `compose.yaml`:*
+
+```
+services:
+ cgit:
+ build: .
+ image: cgit
+ container_name: cgit
+ ports:
+ - 8080:80
+ volumes:
+ - CGITRC:/etc/cgit:ro
+ - COMMIT_FILTER:/usr/local/lib/cgit/filters/commit
+ - ABOUT:/srv/cgit:ro
+ - REPOSITORIES:/srv/git:ro
+```
+
+Browse the website [here](http://localhost:8080).
+
+# Configuration
+
+## Repository specific `cgitrc`
+
+Add a `cgitrc` file at the root of a repository to configure it for cgit. Note that this only works with the `scan-path` setting.
+
+*Example `cgitrc`:*
+
+```
+desc=Repository description
+owner=Repository owner
+section=Repository section
+```
diff --git a/services/nginx/Dockerfile b/services/nginx/Dockerfile
new file mode 100644
index 0000000..bb8e645
--- /dev/null
+++ b/services/nginx/Dockerfile
@@ -0,0 +1,3 @@
+FROM nginx:1.29.4-trixie
+ADD fs.tar.gz /
+CMD ["/sbin/cmd.bash"]
diff --git a/services/nginx/fs/etc/nginx/templates/default.conf.template b/services/nginx/fs/etc/nginx/templates/default.conf.template
new file mode 100644
index 0000000..306a074
--- /dev/null
+++ b/services/nginx/fs/etc/nginx/templates/default.conf.template
@@ -0,0 +1,45 @@
+server {
+ listen 80;
+ listen [::]:80;
+
+ server_name ${NGINX__HOST}
+ www.${NGINX__HOST}
+ dav.${NGINX__HOST}
+ git.${NGINX__HOST};
+
+ # Prevent nginx HTTP Server Detection
+ server_tokens off;
+
+ return 301 https://$host$request_uri;
+}
+
+server {
+ listen 443 ssl;
+ listen [::]:443 ssl;
+
+ server_name ${NGINX__HOST} www.${NGINX__HOST};
+
+ ssl_certificate /run/secrets/server.crt;
+ ssl_certificate_key /run/secrets/server.key;
+
+ location / {
+ root /srv;
+ }
+}
+
+server {
+ listen 443 ssl default_server;
+ listen [::]:443 ssl default_server;
+
+ server_name _;
+
+ ssl_certificate /run/secrets/server.crt;
+ ssl_certificate_key /run/secrets/server.key;
+
+ return 444;
+}
+
+# Docker embedded DNS server
+resolver 127.0.0.11 valid=2s;
+
+include /etc/nginx/conf.d/services/*.conf;
diff --git a/services/nginx/fs/etc/nginx/templates/services/cgit.conf.template b/services/nginx/fs/etc/nginx/templates/services/cgit.conf.template
new file mode 100644
index 0000000..c0fa070
--- /dev/null
+++ b/services/nginx/fs/etc/nginx/templates/services/cgit.conf.template
@@ -0,0 +1,17 @@
+server {
+ listen 443 ssl;
+ listen [::]:443 ssl;
+
+ server_name git.${NGINX__HOST};
+
+ ssl_certificate /run/secrets/server.crt;
+ ssl_certificate_key /run/secrets/server.key;
+
+ location / {
+ proxy_pass http://cgit:80;
+ proxy_set_header Host $host;
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ }
+}
diff --git a/services/nginx/fs/etc/nginx/templates/services/radicale.conf.template b/services/nginx/fs/etc/nginx/templates/services/radicale.conf.template
new file mode 100644
index 0000000..d6e4617
--- /dev/null
+++ b/services/nginx/fs/etc/nginx/templates/services/radicale.conf.template
@@ -0,0 +1,19 @@
+server {
+ listen 443 ssl;
+ listen [::]:443 ssl;
+
+ server_name dav.${NGINX__HOST};
+
+ ssl_certificate /run/secrets/server.crt;
+ ssl_certificate_key /run/secrets/server.key;
+
+ location / {
+ proxy_pass http://radicale:5232;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Host $host;
+ proxy_set_header X-Forwarded-Port $server_port;
+ proxy_set_header X-Forwarded-Proto $scheme;
+ proxy_set_header Host $http_host;
+ proxy_pass_header Authorization;
+ }
+}
diff --git a/services/nginx/fs/sbin/cmd.bash b/services/nginx/fs/sbin/cmd.bash
new file mode 100755
index 0000000..e024b4f
--- /dev/null
+++ b/services/nginx/fs/sbin/cmd.bash
@@ -0,0 +1,11 @@
+#!/usr/bin/bash
+set -eu
+
+# Install sensitive data in tmpfs
+install --mode 400 /run/host_secrets/server.crt /run/secrets/server.crt
+install --mode 400 /run/host_secrets/server.key /run/secrets/server.key
+
+# We have to run the entrypoint again
+# Because if the first positional parameter is not "nginx" or "nginx-debug" the scripts in /docker-entrypoint.d are not ran.
+# https://github.com/nginx/docker-nginx/blob/master/stable/debian/docker-entrypoint.sh
+exec /docker-entrypoint.sh nginx -g "daemon off;"
diff --git a/services/radicale/Dockerfile b/services/radicale/Dockerfile
new file mode 100644
index 0000000..d6e850b
--- /dev/null
+++ b/services/radicale/Dockerfile
@@ -0,0 +1,3 @@
+FROM tomsquest/docker-radicale:3.5.10.0
+ADD fs.tar.gz /
+CMD su-exec radicale /sbin/cmd.sh
diff --git a/services/radicale/fs/etc/radicale/conf.ini b/services/radicale/fs/etc/radicale/conf.ini
new file mode 100644
index 0000000..2af4af9
--- /dev/null
+++ b/services/radicale/fs/etc/radicale/conf.ini
@@ -0,0 +1,14 @@
+[server]
+
+hosts = localhost:5232, radicale:5232
+
+[auth]
+
+type = htpasswd
+htpasswd_filename = /etc/radicale/users/.htpasswd
+htpasswd_encryption = bcrypt
+
+[storage]
+
+filesystem_folder = /data/collections
+hook = git add -A && (git diff --cached --quiet || git commit -m "Changes by \"%(user)s\"")
diff --git a/services/radicale/fs/sbin/cmd.sh b/services/radicale/fs/sbin/cmd.sh
new file mode 100755
index 0000000..4d09e75
--- /dev/null
+++ b/services/radicale/fs/sbin/cmd.sh
@@ -0,0 +1,33 @@
+#!/bin/sh
+set -eu
+
+conf=/etc/radicale/conf.ini
+
+if [ ! -d /data/collections/.git ]; then
+ # Initialize git repository (for storage)
+ echo "Starting server..."
+ /venv/bin/radicale --config "$conf" --logging-level error &
+ radicale_pid=$!
+ echo "Waiting for server to start..."
+ until curl -sf http://127.0.0.1:5232; do sleep 1; done
+ echo "Server started"
+
+ cd /data/collections
+ git init --initial-branch=radicale
+ git config user.name radicale
+ git config user.email radicale@domain.tld
+ cat <<EOF >.gitignore
+.Radicale.cache
+.Radicale.lock
+.Radicale.tmp-*
+EOF
+ git add -A && (git diff --cached --quiet || git commit -m "Initialization commit")
+
+ echo "Restarting server..."
+ kill "$radicale_pid"
+ wait "$radicale_pid"
+else
+ echo "Initialization skipped"
+fi
+
+exec /venv/bin/radicale --config "$conf"