servers/psmf: add psmf server docs, nginx/tailscale config, and data-sync

Document the psmf host (DNS, second tailscale netns instance, nginx
vhosts, migration plan) and add the docker-dev-22 psmf-data-sync
service plus the storage-23 pg_hba.conf entry it needs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-09 00:05:39 +02:00
parent d541f2e1d2
commit 6efa069b12
18 changed files with 624 additions and 0 deletions

View File

@@ -0,0 +1,157 @@
## disable systemd-resolved
```bash
sudo mkdir -p /etc/systemd/resolved.conf.d
sudo tee /etc/systemd/resolved.conf.d/disable-stub.conf <<EOF
[Resolve]
DNSStubListener=no
EOF
# Fix /etc/resolv.conf symlink so the box itself still resolves
sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf
sudo systemctl restart systemd-resolved
```
## dnsmasq in ts2 namespace
```bash
sudo mkdir -p /etc/dnsmasq-ts2
sudo tee /etc/dnsmasq-ts2/dnsmasq.conf <<'EOF'
# Inside ts2 netns — tailscale0 is the only real interface.
# Bind to all (namespace is isolated, so this is safe).
no-resolv
no-hosts
# Upstreams (explicit, no /etc/resolv.conf dependency inside netns)
server=1.1.1.1
server=9.9.9.9
# Zone
local=/intranet/
domain=intranet
address=/psmf.intranet/100.90.25.77
address=/psmf-new.intranet/100.90.25.77
cache-size=1000
log-facility=/var/log/dnsmasq-ts2.log
user=dnsmasq
pid-file=/run/dnsmasq-ts2.pid
EOF
```
```bash
# systemd-unit
sudo tee /etc/systemd/system/dnsmasq-ts2.service <<'EOF'
[Unit]
Description=dnsmasq inside ts2 netns (intranet zone)
After=network-online.target
Wants=network-online.target
# If you have a unit that sets up the ts2 namespace + tailscaled inside it,
# add it here, e.g.:
# Requires=tailscaled-ts2.service
# After=tailscaled-ts2.service
[Service]
Type=simple
NetworkNamespacePath=/var/run/netns/ts2
ExecStartPre=/usr/sbin/dnsmasq --test -C /etc/dnsmasq-ts2/dnsmasq.conf
ExecStart=/usr/sbin/dnsmasq -k -C /etc/dnsmasq-ts2/dnsmasq.conf
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
```
```bash
# validate and start
sudo systemctl daemon-reload
sudo systemctl enable --now dnsmasq-ts2
sudo systemctl status dnsmasq-ts2 --no-pager
sudo ip netns exec ts2 ss -lnup | grep :53
sudo ip netns exec ts2 dig @127.0.0.1 psmf.intranet +short
sudo ip netns exec ts2 dig @127.0.0.1 cloudflare.com +short
```
## dnsmasq in root namespace
```bash
sudo mkdir -p /etc/dnsmasq-root
sudo tee /etc/dnsmasq-root/dnsmasq.conf <<'EOF'
# Root netns instance. Stock dnsmasq.service must be disabled/masked
# so it doesn't fight us for port 53.
# systemd-resolved stub listener is disabled (see top of this file).
# Only listen on these — don't grab :53 on every interface.
bind-interfaces
interface=br0
interface=tailscale0
# Upstreams (explicit, no /etc/resolv.conf lookup)
no-resolv
no-hosts
server=1.1.1.1
server=9.9.9.9
# Zone
local=/intranet/
domain=intranet
address=/psmf.intranet/100.90.25.77
address=/psmf-new.intranet/100.90.25.77
cache-size=1000
log-facility=/var/log/dnsmasq-root.log
user=dnsmasq
pid-file=/run/dnsmasq-root.pid
EOF
```
```bash
# systemd-unit
sudo tee /etc/systemd/system/dnsmasq-root.service <<'EOF'
[Unit]
Description=dnsmasq in root netns (intranet zone)
After=network-online.target
Wants=network-online.target
# Make sure the stock apt dnsmasq is out of the way:
# sudo systemctl disable --now dnsmasq
# sudo systemctl mask dnsmasq
[Service]
Type=simple
ExecStartPre=/usr/sbin/dnsmasq --test -C /etc/dnsmasq-root/dnsmasq.conf
ExecStart=/usr/sbin/dnsmasq -k -C /etc/dnsmasq-root/dnsmasq.conf
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
EOF
```
```bash
# validate and start
sudo systemctl daemon-reload
sudo systemctl enable --now dnsmasq-root
sudo systemctl status dnsmasq-root --no-pager
# verify it's bound only on br0 + tailscale0 (not on lo, not 0.0.0.0)
sudo ss -lnup | grep :53
# smoke test against the host's own br0 / tailscale0 IP
BR0_IP=$(ip -4 -o addr show br0 | awk '{print $4}' | cut -d/ -f1)
TS0_IP=$(ip -4 -o addr show tailscale0 | awk '{print $4}' | cut -d/ -f1)
dig @"$BR0_IP" psmf.intranet +short
dig @"$TS0_IP" psmf.intranet +short
dig @"$BR0_IP" cloudflare.com +short
```

View File

@@ -0,0 +1,28 @@
# Reverse proxy: horst-2 default site -> horst-1 Apache (10.0.0.5)
# Deployed during the horst-1 -> horst-2 migration.
# See servers/psmf/migration-plan.md.
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
# Long bodies / uploads from the legacy PHP intranet app.
client_max_body_size 64m;
location / {
proxy_pass http://10.0.0.5;
proxy_http_version 1.1;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_connect_timeout 10s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}

View File

@@ -0,0 +1,27 @@
# Reverse proxy: psmf-new.intranet -> dockerized PHP intranet app on horst-2 (127.0.0.1:8080)
# See servers/psmf/migration-plan.md and servers/psmf/dns-configuration.md.
server {
listen 80;
listen [::]:80;
server_name psmf-new.intranet;
# Long bodies / uploads from the PHP intranet app.
client_max_body_size 64m;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_http_version 1.1;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_connect_timeout 10s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}

View File

@@ -0,0 +1,28 @@
# Reverse proxy: psmf.intranet -> horst-1 Apache (10.0.0.5)
# Same target as the default site, but bound to the psmf.intranet host header.
# See servers/psmf/migration-plan.md and servers/psmf/dns-configuration.md.
server {
listen 80;
listen [::]:80;
server_name psmf.intranet;
# Long bodies / uploads from the legacy PHP intranet app.
client_max_body_size 64m;
location / {
proxy_pass http://10.0.0.5;
proxy_http_version 1.1;
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;
proxy_set_header X-Forwarded-Host $host;
proxy_connect_timeout 10s;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}

View File

@@ -0,0 +1,15 @@
services:
app:
image: gitea.home.hrajfrisbee.cz/psmf/psmf-intranet:1.03
ports:
- "8080:8080"
environment:
DB_HOST: ${DB_HOST:-host.docker.internal}
DB_NAME: ${DB_NAME:-psmf}
DB_USER: ${DB_USER:-psmf}
DB_PASS: ${DB_PASS:-psmf}
# PSMF_BASE_URL: http://127.0.0.1:8080 # override for PDF export when behind a proxy
PSMF_HEADER_TEXT: "Vítejte na intranetu PSMF (PHP {PHP_VERSION})"
PSMF_HEADER_BG: "#a0b000"
PSMF_SHOW_PHP_VERSION: "1"
restart: unless-stopped

View File

@@ -0,0 +1,21 @@
# location: /etc/systemd/system/tailscaled-ts2.service
[Unit]
Description=tailscaled in ts2 netns
After=ts2-netns.service network-online.target
Requires=ts2-netns.service
BindsTo=ts2-netns.service
[Service]
ExecStart=/usr/sbin/ip netns exec ts2 /usr/sbin/tailscaled \
--state=/var/lib/tailscale-ts2/tailscaled.state \
--socket=/run/tailscale-ts2/tailscaled.sock \
--port=41642 \
--tun=tailscale0
Restart=on-failure
RestartSec=5s
RuntimeDirectory=tailscale-ts2
StateDirectory=tailscale-ts2
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# best-effort cleanup, don't fail on missing pieces
# location: /usr/local/sbin/ts2-netns-down
iptables -t nat -D POSTROUTING -s 10.200.0.0/30 -o br0 -j MASQUERADE 2>/dev/null || true
iptables -D FORWARD -i veth-ts2 -o br0 -j ACCEPT 2>/dev/null || true
iptables -D FORWARD -o veth-ts2 -i br0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || true
ip link del veth-ts2 2>/dev/null || true
ip netns del ts2 2>/dev/null || true
rm -rf /etc/netns/ts2

View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# location: /usr/local/sbin/ts2-netns-up
set -e
# bail out clean if anything already exists from a previous run
ip netns list | grep -qw ts2 || ip netns add ts2
ip link show veth-ts2 &>/dev/null || \
ip link add veth-ts2 type veth peer name veth-ts2-ns
# move peer into ns only if it's still on the host
ip link show veth-ts2-ns &>/dev/null && \
ip link set veth-ts2-ns netns ts2
ip addr add 10.200.0.1/30 dev veth-ts2 2>/dev/null || true
ip link set veth-ts2 up
ip -n ts2 addr add 10.200.0.2/30 dev veth-ts2-ns 2>/dev/null || true
ip -n ts2 link set veth-ts2-ns up
ip -n ts2 link set lo up
ip -n ts2 route add default via 10.200.0.1 2>/dev/null || true
mkdir -p /etc/netns/ts2
echo "nameserver 1.1.1.1" > /etc/netns/ts2/resolv.conf
sysctl -wq net.ipv4.ip_forward=1
# -C checks if rule exists, -A appends only if missing
iptables -t nat -C POSTROUTING -s 10.200.0.0/30 -o br0 -j MASQUERADE 2>/dev/null || \
iptables -t nat -A POSTROUTING -s 10.200.0.0/30 -o br0 -j MASQUERADE
iptables -C FORWARD -i veth-ts2 -o br0 -j ACCEPT 2>/dev/null || \
iptables -A FORWARD -i veth-ts2 -o br0 -j ACCEPT
iptables -C FORWARD -o veth-ts2 -i br0 -m state --state RELATED,ESTABLISHED -j ACCEPT 2>/dev/null || \
iptables -A FORWARD -o veth-ts2 -i br0 -m state --state RELATED,ESTABLISHED -j ACCEPT
# iptables rules + sysctl inside ts2 ns
# Forwarding inside ts2
ip netns exec ts2 sysctl -wq net.ipv4.ip_forward=1
# DNAT incoming ssh to host
ip netns exec ts2 iptables -t nat -C PREROUTING -i tailscale0 -p tcp --dport 22 -j DNAT --to-destination 10.200.0.1:22 2>/dev/null || \
ip netns exec ts2 iptables -t nat -A PREROUTING -i tailscale0 -p tcp --dport 22 -j DNAT --to-destination 10.200.0.1:22
# DNAT incoming http to host nginx
ip netns exec ts2 iptables -t nat -C PREROUTING -i tailscale0 -p tcp --dport 80 -j DNAT --to-destination 10.200.0.1:80 2>/dev/null || \
ip netns exec ts2 iptables -t nat -A PREROUTING -i tailscale0 -p tcp --dport 80 -j DNAT --to-destination 10.200.0.1:80
# DNAT incoming 8080 to docker web app in root ns
ip netns exec ts2 iptables -t nat -C PREROUTING -i tailscale0 -p tcp --dport 8080 -j DNAT --to-destination 10.200.0.1:8080 2>/dev/null || \
ip netns exec ts2 iptables -t nat -A PREROUTING -i tailscale0 -p tcp --dport 8080 -j DNAT --to-destination 10.200.0.1:8080
ip netns exec ts2 iptables -t nat -C POSTROUTING -o veth-ts2-ns -j MASQUERADE 2>/dev/null || \
ip netns exec ts2 iptables -t nat -A POSTROUTING -o veth-ts2-ns -j MASQUERADE

View File

@@ -0,0 +1,17 @@
# /etc/systemd/system/ts2-netns.service
[Unit]
Description=ts2 netns + veth + NAT
After=network-online.target
Wants=network-online.target
# tie tailscaled lifecycle to this
Before=tailscaled-ts2.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/ts2-netns-up
ExecStop=/usr/local/sbin/ts2-netns-down
[Install]
WantedBy=multi-user.target

View File

@@ -0,0 +1,2 @@
# location: /etc/netns/ts2/resolv.conf
nameserver 1.1.1.1

View File

@@ -0,0 +1,6 @@
# backups
15 11,15,19,22 * * * root /srv/bin/psmf-cli db backup --env-file ~/.psmf/horst-2.env --db psmf --dir /srv/backups/horst-2 -v >> /var/log/psmf-backup.log 2>&1
17 11,15,19,22 * * * root /srv/bin/psmf-cli db backup --env-file ~/.psmf/horst.env --db psmf --dir /srv/backups/horst -v >> /var/log/psmf-backup.log 2>&1
# prune backups
20 11,15,19,22 * * * root /srv/bin/psmf-cli db prune-backups --db psmf --dir /srv/backups/horst-2 --keep-within 2w --keep-weekly 8 --keep-monthly 120 >> /var/log/psmf-backup.log 2>&1
21 11,15,19,22 * * * root /srv/bin/psmf-cli db prune-backups --db psmf --dir /srv/backups/horst --keep-within 2w --keep-weekly 8 --keep-monthly 120 >> /var/log/psmf-backup.log 2>&1

View File

@@ -0,0 +1,93 @@
# Migration from horst-1 to horst-2
## postgres database
```bash
# [DONE] install postgres
apt update
apt install -y curl ca-certificates gnupg lsb-release
# PGDG signing key (keyring-style, not the deprecated apt-key)
sudo install -d /usr/share/postgresql-common/pgdg
sudo curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
-o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc
# repo
echo "deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] \
https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" \
| sudo tee /etc/apt/sources.list.d/pgdg.list
sudo apt update
sudo apt install -y postgresql-18 postgresql-contrib-18
# backup database from horst-1
# users backup
pg_dumpall --globals-only -h 10.0.0.5 -U postgres > /tmp/pg-dumpall-globals
# data backup
pg_dump -h 10.0.0.5 -U postgres -C psmf > /tmp/psmf.pgsql
# restore backup of database from horst-1
# create users
cat /tmp/pg-dumpall-globals | psql
# restore psmf database
cat /tmp/psmf.pgsql | psql
# install docker oin horst-2
# Docker's GPG key (keyring, not apt-key)
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \
-o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc
# repo
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install -y \
docker-ce docker-ce-cli containerd.io \
docker-buildx-plugin docker-compose-plugin
# [DONE] dockerize php intranet app
# dockerized + upgraded to PHP 8.3.31
# [DONE] start psmf app on horst-2 and point it to database on horst
# [TODO] point php app on horst-1 to horst-2
# validate application is still working
# [OK] - wkhtml - PDF printing of html pages
# prepare local backups
# validate if pg sync to api still works from horst-1
# set sync to api on horst-2
# install nginx and configure default site as reverse proxy to horst-1
sudo apt-get update
sudo apt-get install -y nginx
# drop the proxy config in place (file lives in this repo at servers/psmf/files/nginx/default)
sudo install -m 0644 -o root -g root \
servers/psmf/files/nginx/default /etc/nginx/sites-available/default
# sites-enabled/default is already a symlink to ../sites-available/default on a stock Ubuntu install
sudo nginx -t
sudo systemctl reload nginx
sudo systemctl enable nginx
# intranet vhosts: psmf.intranet -> horst-1, psmf-new.intranet -> local docker app
# (depends on the dnsmasq setup in servers/psmf/dns-configuration.md)
sudo install -m 0644 -o root -g root \
servers/psmf/files/nginx/psmf.intranet /etc/nginx/sites-available/psmf.intranet
sudo install -m 0644 -o root -g root \
servers/psmf/files/nginx/psmf-new.intranet /etc/nginx/sites-available/psmf-new.intranet
sudo ln -sf ../sites-available/psmf.intranet /etc/nginx/sites-enabled/psmf.intranet
sudo ln -sf ../sites-available/psmf-new.intranet /etc/nginx/sites-enabled/psmf-new.intranet
sudo nginx -t
sudo systemctl reload nginx
```

33
servers/psmf/readme.md Normal file
View File

@@ -0,0 +1,33 @@
# Servers
### Horst
- ip: 10.0.0.5
- os: CentOS release 6.9 (Final)
- services:
- apache tcp/80
- postgres tcp/5432
- samba (not really in use)
- cronjobs
- /srv/bin/pg_psmf_dump.sh
- /srv/bin/psmf-prepare-tables.sh 2023 2027
- /srv/bin/psmf-api-sync.sh
- specials:
- some binaries/libraries relevant to PDF printing from the app WKHTML
```bash
# crontab content
00 11,15,19,22 * * * root /srv/bin/pg_psmf_dump.sh
55 10,14,18,21 * * * root /srv/bin/psmf-prepare-tables.sh 2023 2027
05 11,15,19,22 * * * root /srv/bin/psmf-api-sync.sh
```
#### Problems
- operating system is already that old, that there is not reasonable and safe upgrade path to something current
### Horst-2
- ip: 10.0.0.6

View File

@@ -0,0 +1,39 @@
```bash
# Create namespace
sudo ip netns add ts2
# veth pair: host side <-> ns side
sudo ip link add veth-ts2 type veth peer name veth-ts2-ns
sudo ip link set veth-ts2-ns netns ts2
# Host side
sudo ip addr add 10.200.0.1/30 dev veth-ts2
sudo ip link set veth-ts2 up
# Namespace side
sudo ip -n ts2 addr add 10.200.0.2/30 dev veth-ts2-ns
sudo ip -n ts2 link set veth-ts2-ns up
sudo ip -n ts2 link set lo up
sudo ip -n ts2 route add default via 10.200.0.1
# DNS inside the netns
sudo mkdir -p /etc/netns/ts2
echo "nameserver 1.1.1.1" | sudo tee /etc/netns/ts2/resolv.conf
# NAT from the netns out to the world (replace eth0 with your egress iface)
sudo sysctl -w net.ipv4.ip_forward=1
sudo iptables -t nat -A POSTROUTING -s 10.200.0.0/30 -o br0 -j MASQUERADE
sudo iptables -A FORWARD -i veth-ts2 -o br0 -j ACCEPT
sudo iptables -A FORWARD -o veth-ts2 -i br0 -m state --state RELATED,ESTABLISHED -j ACCEPT
# quick sanity check
sudo ip netns exec ts2 curl https://controlplane.tailscale.com
# expect: "OK"
```
```bash
# ts2 alias
echo 'alias ts2="sudo ip netns exec ts2 tailscale --socket=/run/tailscale-ts2/tailscaled.sock"' \
| sudo tee /etc/profile.d/tailscale-ts2.sh
```