mirror of
https://github.com/aykhans/my-self-host-services.git
synced 2026-07-13 20:37:40 +00:00
Compare commits
8
Commits
b1d78905ca
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1f876b1402
|
||
|
|
2b8b8b6514
|
||
|
|
b53c6b3d4a
|
||
|
|
adc45ebc56
|
||
|
|
7f8bfb81c0 | ||
|
|
e9ae811fcf
|
||
|
|
3c46b0de47
|
||
|
|
6d9c4e7d39
|
@@ -20,6 +20,8 @@
|
|||||||
- 9009-9013/tcp (relay)
|
- 9009-9013/tcp (relay)
|
||||||
- **SFTPGo**
|
- **SFTPGo**
|
||||||
- 2022/tcp (SFTP)
|
- 2022/tcp (SFTP)
|
||||||
|
- **Gitea**
|
||||||
|
- 2222/tcp (SSH)
|
||||||
- **WireGuard Easy**
|
- **WireGuard Easy**
|
||||||
- 51820/udp (WireGuard)
|
- 51820/udp (WireGuard)
|
||||||
|
|
||||||
@@ -89,24 +91,9 @@ Launch all services with the following command:
|
|||||||
|
|
||||||
### 6. Host Firewall Bouncer (CrowdSec, nftables)
|
### 6. Host Firewall Bouncer (CrowdSec, nftables)
|
||||||
|
|
||||||
The Caddy bouncer protects HTTP services. Stalwart's mail ports (25/465/587/143/993/110/995/4190) bypass Caddy, so install a firewall bouncer on the host. CrowdSec packages live on PackageCloud, not in the default apt repos, so add the repo first:
|
The Caddy bouncer protects HTTP services. Stalwart's mail ports (25/465/587/143/993/110/995/4190) bypass Caddy, so run a CrowdSec **firewall bouncer** on the host itself (nftables backend).
|
||||||
|
|
||||||
```sh
|
Configure it to talk to the engine's LAPI on `127.0.0.1:18080` (published by the crowdsec container) and give it an `api_key` equal to `CROWDSEC_BOUNCER_KEY_FW` from step 4. Do NOT install the CrowdSec engine on the host, it already runs in Docker. For installing and configuring the bouncer itself, see the official docs: https://docs.crowdsec.net/u/bouncers/firewall/ (repo: https://github.com/crowdsecurity/cs-firewall-bouncer).
|
||||||
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash
|
|
||||||
sudo apt install crowdsec-firewall-bouncer-nftables
|
|
||||||
```
|
|
||||||
|
|
||||||
Do NOT use `install.crowdsec.net` (that installs the engine too, which we already run in Docker).
|
|
||||||
|
|
||||||
Patch the default config (the package writes `api_url: http://127.0.0.1:8080/` but our LAPI is on 18080):
|
|
||||||
|
|
||||||
```sh
|
|
||||||
FW_KEY=$(grep '^CROWDSEC_BOUNCER_KEY_FW=' ./crowdsec/.env | cut -d= -f2)
|
|
||||||
sudo sed -i "s|^api_url:.*|api_url: http://127.0.0.1:18080/|" /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml
|
|
||||||
sudo sed -i "s|^api_key:.*|api_key: $FW_KEY|" /etc/crowdsec/bouncers/crowdsec-firewall-bouncer.yaml
|
|
||||||
sudo systemctl enable --now crowdsec-firewall-bouncer
|
|
||||||
sudo systemctl status crowdsec-firewall-bouncer --no-pager
|
|
||||||
```
|
|
||||||
|
|
||||||
Verify:
|
Verify:
|
||||||
|
|
||||||
@@ -117,30 +104,18 @@ sudo nft list ruleset | grep crowdsec # kernel-level rules in place
|
|||||||
|
|
||||||
## Backups (optional, restic)
|
## Backups (optional, restic)
|
||||||
|
|
||||||
This is **optional** — use it only if you want off-site, encrypted backups of your service data. Here it is done with [restic](https://github.com/restic/restic) over SFTP (e.g. a Hetzner Storage Box). restic encrypts client-side, deduplicates and compresses, so no separate `tar`/`gzip` is needed. Run as **root**, since Docker-created service data is mostly root-owned.
|
This is **optional**, use it only if you want off-site, encrypted backups of your service data. Here it is done with [restic](https://github.com/restic/restic) over SFTP (e.g. a Hetzner Storage Box). restic encrypts client-side, deduplicates and compresses, so no separate `tar`/`gzip` is needed. Run as **root**, since Docker-created service data is mostly root-owned.
|
||||||
|
|
||||||
Replace the placeholders: `USER@HOST:PORT` (SFTP target), `/path/to/restic-repo` (repo path on the server), `YOUR_PASSWORD` (losing it means the backup is unrecoverable), `/path/to/services` (directory to back up). Do not commit real secrets to git.
|
Installing restic and scheduling the backup depend on the host and are out of scope here. Replace the placeholders: `USER@HOST:PORT` (SFTP target), `/path/to/restic-repo` (repo path on the server), `YOUR_PASSWORD` (losing it means the backup is unrecoverable), `/path/to/services` (directory to back up). Do not commit real secrets to git.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# 1. Install restic and set up passwordless SSH to the target (as root)
|
|
||||||
sudo apt install restic
|
|
||||||
sudo -i
|
|
||||||
ssh-keygen -t ed25519 && ssh-copy-id -p PORT USER@HOST # no passphrase
|
|
||||||
|
|
||||||
# 2. Backup script: /usr/local/bin/restic-backup.sh (chmod 700)
|
|
||||||
#!/bin/bash
|
|
||||||
export RESTIC_REPOSITORY="sftp://USER@HOST:PORT//path/to/restic-repo" # note the double slash before an absolute path
|
export RESTIC_REPOSITORY="sftp://USER@HOST:PORT//path/to/restic-repo" # note the double slash before an absolute path
|
||||||
export RESTIC_PASSWORD="YOUR_PASSWORD"
|
export RESTIC_PASSWORD="YOUR_PASSWORD"
|
||||||
|
|
||||||
|
restic init # once
|
||||||
restic backup /path/to/services --compression max
|
restic backup /path/to/services --compression max
|
||||||
restic forget --keep-last 3 --prune # keep only the 3 newest snapshots
|
restic forget --keep-last 3 --prune # keep only the 3 newest snapshots
|
||||||
|
restic snapshots
|
||||||
# 3. Initialize once, then test
|
|
||||||
restic init
|
|
||||||
/usr/local/bin/restic-backup.sh && restic snapshots
|
|
||||||
|
|
||||||
# 4. Schedule via cron (3x/day at 06:00, 14:00, 22:00)
|
|
||||||
sudo crontab -e
|
|
||||||
# 0 6,14,22 * * * /usr/local/bin/restic-backup.sh >> /var/log/restic-backup.log 2>&1
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Restore (from any machine with restic + repo access + password, as root):
|
Restore (from any machine with restic + repo access + password, as root):
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
source: journalctl
|
source: journalctl
|
||||||
journalctl_filter:
|
journalctl_filter:
|
||||||
- "_SYSTEMD_UNIT=ssh.service"
|
# NixOS names the OpenSSH unit "sshd.service"; Debian/Ubuntu use "ssh.service".
|
||||||
|
- "_SYSTEMD_UNIT=sshd.service"
|
||||||
labels:
|
labels:
|
||||||
type: syslog
|
type: syslog
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# URL of the Gitea instance
|
# URL of the Gitea instance
|
||||||
GITEA_INSTANCE_URL=http://gitea:3000
|
GITEA_INSTANCE_URL=http://gitea:3000
|
||||||
|
# Domain advertised in SSH clone URLs (e.g. gitea.aykhans.me)
|
||||||
|
GITEA_SSH_DOMAIN=
|
||||||
# How to get the token: https://docs.gitea.com/usage/actions/act-runner#obtain-a-registration-token
|
# How to get the token: https://docs.gitea.com/usage/actions/act-runner#obtain-a-registration-token
|
||||||
GITEA_RUNNER_REGISTRATION_TOKEN=
|
GITEA_RUNNER_REGISTRATION_TOKEN=
|
||||||
# UID and GID of the user that should run the runner (e.g. USER_UID=1000, USER_GID=1000)
|
# UID and GID of the user that should run the runner (e.g. USER_UID=1000, USER_GID=1000)
|
||||||
|
|||||||
@@ -15,10 +15,14 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
- USER_UID=${USER_UID}
|
- USER_UID=${USER_UID}
|
||||||
- USER_GID=${USER_GID}
|
- USER_GID=${USER_GID}
|
||||||
|
- GITEA__server__SSH_DOMAIN=${GITEA_SSH_DOMAIN}
|
||||||
|
- GITEA__server__SSH_PORT=2222
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- gitea
|
- gitea
|
||||||
- caddy
|
- caddy
|
||||||
|
ports:
|
||||||
|
- "2222:22"
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
- /etc/timezone:/etc/timezone:ro
|
- /etc/timezone:/etc/timezone:ro
|
||||||
|
|||||||
@@ -70,7 +70,31 @@ generate_env_files() {
|
|||||||
print_success ".env files generated."
|
print_success ".env files generated."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Some containers run as a fixed non-root uid and bind-mount their data from
|
||||||
|
# this repo. A fresh `git clone` (or a non-root restore) leaves that data owned
|
||||||
|
# by the cloning user, so those services can't write to it. Docker does not
|
||||||
|
# chown bind mounts, so reset ownership to each container's uid before starting.
|
||||||
|
# Add a line here whenever a new service pins a non-root `user:`.
|
||||||
|
fix_permissions() {
|
||||||
|
echo "Fixing data ownership for fixed-uid services..."
|
||||||
|
# Only services that run as a fixed non-root uid AND write to a bind-mounted
|
||||||
|
# data dir need this. Root containers write fine; images like mysql/gitea
|
||||||
|
# chown their own data via a root entrypoint; grafana/prometheus here only
|
||||||
|
# read config or use no writable bind mount, so they are intentionally absent.
|
||||||
|
sudo chown -R 1001:1001 ./stalwart/data # stalwart: compose user "1001:1001"
|
||||||
|
sudo chown -R 1000:1000 ./sftpgo/data # sftpgo: image default user 1000
|
||||||
|
}
|
||||||
|
|
||||||
start_services() {
|
start_services() {
|
||||||
|
fix_permissions
|
||||||
|
|
||||||
|
# Ensure ~/.docker/config.json is a FILE before any container mounts it.
|
||||||
|
# watchtower bind-mounts it; if it is missing, the Docker daemon creates it
|
||||||
|
# as a root-owned directory, which then breaks every docker CLI call.
|
||||||
|
mkdir -p ~/.docker
|
||||||
|
[ -d ~/.docker/config.json ] && sudo rmdir ~/.docker/config.json
|
||||||
|
[ -f ~/.docker/config.json ] || echo '{}' > ~/.docker/config.json
|
||||||
|
|
||||||
docker network create caddy 2>/dev/null
|
docker network create caddy 2>/dev/null
|
||||||
docker network create grafana 2>/dev/null
|
docker network create grafana 2>/dev/null
|
||||||
docker network create gitea 2>/dev/null
|
docker network create gitea 2>/dev/null
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ services:
|
|||||||
- -housekeeping_interval=3s
|
- -housekeeping_interval=3s
|
||||||
- -storage_duration=2m
|
- -storage_duration=2m
|
||||||
- -docker_only
|
- -docker_only
|
||||||
|
- -containerd=/var/run/docker/containerd/containerd.sock
|
||||||
logging:
|
logging:
|
||||||
driver: "json-file"
|
driver: "json-file"
|
||||||
options:
|
options:
|
||||||
|
|||||||
Reference in New Issue
Block a user