Compare commits
6 Commits
687ffa1017
...
df4caf48d0
| Author | SHA1 | Date |
|---|---|---|
|
|
df4caf48d0 | 2 months ago |
|
|
b39b9af978 | 2 months ago |
|
|
681058a4c5 | 2 months ago |
|
|
bc6c9fe481 | 2 months ago |
|
|
acb81bf342 | 2 months ago |
|
|
3c87e0c74b | 2 months ago |
21 changed files with 468 additions and 26 deletions
@ -1,10 +0,0 @@ |
|||
# Network settings |
|||
port 6379 |
|||
bind 127.0.0.1 |
|||
|
|||
# Set a password for Redis |
|||
requirepass nextcloud |
|||
|
|||
# Hybrid mode (AOF + RDB) |
|||
appendonly yes |
|||
aof-use-rdb-preamble yes |
|||
@ -0,0 +1,12 @@ |
|||
##
|
|||
## Makefile for ntfy quadlet
|
|||
##
|
|||
|
|||
DEPENDENCIES = postgresql traefik |
|||
|
|||
# ntfy quadlet is mapped to the 10027 user (ntfy) and 10000 group (itix-svc)
|
|||
PROJECT_UID = 10027 |
|||
PROJECT_GID = 10000 |
|||
|
|||
# Include common Makefile
|
|||
include ../../scripts/common.mk |
|||
@ -0,0 +1,62 @@ |
|||
# Podman Quadlet: ntfy |
|||
|
|||
## Overview |
|||
|
|||
ntfy is a simple HTTP-based pub-sub notification service started as a Podman Quadlet. It lets you send push notifications to your phone or desktop via scripts from any computer. |
|||
|
|||
This cookbook: |
|||
|
|||
- Runs ntfy as a rootless container with minimal privileges (UID 10027). |
|||
- Uses PostgreSQL as the database backend (requires the `postgresql` cookbook). |
|||
- Stores attachment cache on virtiofs (`/var/lib/virtiofs/data/ntfy`). |
|||
- Exposes ntfy through Traefik (requires the `traefik` cookbook). |
|||
- Includes health checks to monitor the service status. |
|||
- Supports automatic container image updates via Podman auto-update. |
|||
|
|||
## Prerequisites |
|||
|
|||
- The `postgresql` cookbook must be installed and running. |
|||
- The `traefik` cookbook must be installed and running. |
|||
- The `base` cookbook must be installed (provides the virtiofs mount). |
|||
- Configuration file `/etc/quadlets/ntfy/server.yml` must exist. |
|||
|
|||
## Usage |
|||
|
|||
Copy and customize the example configuration: |
|||
|
|||
```sh |
|||
sudo cp config/examples/server.yml /etc/quadlets/ntfy/server.yml |
|||
sudo vi /etc/quadlets/ntfy/server.yml |
|||
``` |
|||
|
|||
In a separate terminal, follow the logs: |
|||
|
|||
```sh |
|||
sudo make tail-logs |
|||
``` |
|||
|
|||
Install the Podman Quadlets and start ntfy: |
|||
|
|||
```sh |
|||
sudo make clean install |
|||
``` |
|||
|
|||
You should see the **ntfy.service** waiting for PostgreSQL to be available, then starting up. |
|||
|
|||
Verify ntfy is running: |
|||
|
|||
```sh |
|||
curl -sSf http://127.0.0.1:8080/v1/health |
|||
``` |
|||
|
|||
Restart the **ntfy.target** unit: |
|||
|
|||
```sh |
|||
sudo systemctl restart ntfy.target |
|||
``` |
|||
|
|||
Finally, remove the quadlets, their configuration and their data: |
|||
|
|||
```sh |
|||
sudo make uninstall clean |
|||
``` |
|||
@ -0,0 +1,101 @@ |
|||
# Specification for ntfy Quadlet Cookbook |
|||
|
|||
You will have to develop a Quadlet cookbook for ntfy.sh, the self-hosted notification server. |
|||
|
|||
## Architecture |
|||
|
|||
Ntfy is a web application, deployed as a container image, available here: `docker.io/binwiederhier/ntfy:v2`. |
|||
|
|||
Ntfy relies on a PostgreSQL database to store its data. It also uses a cache directory for attachments (that you have to store on virtiofs). |
|||
You will also have to expose it through Traefik. |
|||
|
|||
## Common requirements |
|||
|
|||
- Each docker image MUST have its quadlet .image file. |
|||
- Each cookbook MUST have a dedicated unique UID. The GID is 10000. |
|||
- Persistent data MUST be stored on virtiofs (`/var/lib/virtiofs/data/ntfy`). |
|||
|
|||
## Sample commands for deployment |
|||
|
|||
You will have to convert the following command to a Quadlet recipe: |
|||
|
|||
```sh |
|||
docker run -v /etc/ntfy:/etc/ntfy -v /var/cache/ntfy:/var/cache/ntfy -e TZ=UTC -p 8080:8080 -u $UID:$GID -it binwiederhier/ntfy serve |
|||
``` |
|||
|
|||
Other example, using Docker Compose: |
|||
|
|||
```yaml |
|||
services: |
|||
ntfy: |
|||
image: binwiederhier/ntfy |
|||
container_name: ntfy |
|||
command: |
|||
- serve |
|||
environment: |
|||
- TZ=UTC # optional: set desired timezone |
|||
user: $UID:$GID # optional: replace with your own user/group or uid/gid |
|||
volumes: |
|||
- /var/cache/ntfy:/var/cache/ntfy |
|||
- /etc/ntfy:/etc/ntfy |
|||
ports: |
|||
- 8080:8080 |
|||
healthcheck: # optional: remember to adapt the host:port to your environment |
|||
test: ["CMD-SHELL", "wget -q --tries=1 http://localhost:8080/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1"] |
|||
interval: 60s |
|||
timeout: 10s |
|||
retries: 3 |
|||
start_period: 40s |
|||
restart: unless-stopped |
|||
init: true # needed, if healthcheck is used. Prevents zombie processes |
|||
``` |
|||
|
|||
## Security |
|||
|
|||
Directly set the UID and GID in the quadlet file (no mapping). |
|||
Use the host network, like other quadlet cookbooks. |
|||
Let's Encrypt certificates will be handled by Traefik, so no need to worry about that in the ntfy cookbook. |
|||
|
|||
## Configuration |
|||
|
|||
The configuration file for ntfy (`/etc/ntfy/server.yml` inside the container) is in YAML format. |
|||
|
|||
```yaml |
|||
# Server |
|||
base-url: "https://ntfy.itix.fr" |
|||
behind-proxy: true |
|||
listen-http: "127.0.0.1:8080" |
|||
|
|||
# Database |
|||
database-url: "postgres://user:pass@host:5432/ntfy" |
|||
|
|||
# Access control |
|||
auth-default-access: "deny-all" |
|||
auth-users: |
|||
# fields are: login:bcrypt-hashed-password:role (admin or user) |
|||
- "admin:$2b$REDACTED:admin" |
|||
enable-login: true |
|||
require-login: true |
|||
|
|||
# Attachments |
|||
attachment-cache-dir: "/var/cache/ntfy/attachments" |
|||
attachment-file-size-limit: "100M" |
|||
attachment-total-size-limit: "50G" |
|||
attachment-expiry-duration: "48h" |
|||
|
|||
# Message cache |
|||
cache-duration: "48h" |
|||
|
|||
# Upstream |
|||
upstream-base-url: "https://ntfy.sh" |
|||
``` |
|||
|
|||
## Useful examples |
|||
|
|||
You can copy the structure of the `miniflux` cookbook, which is also a web application relying on a database and exposed through Traefik. |
|||
For virtiofs persistent storage, have a look at the `redis` or `postgresql` cookbooks. |
|||
|
|||
## Useful links |
|||
|
|||
- [Installation guide](https://ntfy.sh/docs/install/) |
|||
- [Configuration reference](https://ntfy.sh/docs/config/) |
|||
@ -0,0 +1,29 @@ |
|||
# Server |
|||
base-url: "http://ntfy/" |
|||
behind-proxy: true |
|||
listen-http: "127.0.0.1:8080" |
|||
|
|||
# Database |
|||
database-url: "postgres://ntfy:ntfy@localhost/ntfy?sslmode=disable" |
|||
|
|||
# Access control |
|||
auth-default-access: "deny-all" |
|||
auth-users: |
|||
# fields are: login:bcrypt-hashed-password:role (admin or user) |
|||
# the following bcrypt hash has been generated with: |
|||
# echo -ne "admin\nadmin" | podman run -i --rm docker.io/binwiederhier/ntfy:v2 user hash |
|||
- "admin:$2a$10$9t74/X77vkvZJ.ZEBOd1aukjxwl5xk7FVtI99ywQ8rdqjPJiY9fHm:admin" |
|||
enable-login: true |
|||
require-login: true |
|||
|
|||
# Attachments (stored on virtiofs) |
|||
attachment-cache-dir: "/var/cache/ntfy/attachments" |
|||
attachment-file-size-limit: "100M" |
|||
attachment-total-size-limit: "50G" |
|||
attachment-expiry-duration: "48h" |
|||
|
|||
# Message cache |
|||
cache-duration: "48h" |
|||
|
|||
# Upstream (for iOS push notifications) |
|||
upstream-base-url: "https://ntfy.sh" |
|||
@ -0,0 +1,49 @@ |
|||
[Unit] |
|||
Description=ntfy - Simple HTTP-based pub-sub notification service |
|||
Documentation=https://docs.ntfy.sh/ |
|||
After=network.target |
|||
RequiresMountsFor=/var/lib/virtiofs/data |
|||
|
|||
# Only start if ntfy has been configured |
|||
ConditionPathExists=/etc/quadlets/ntfy/server.yml |
|||
|
|||
# Start/stop this unit when the target is started/stopped |
|||
PartOf=ntfy.target |
|||
|
|||
[Container] |
|||
ContainerName=ntfy |
|||
Image=ntfy.image |
|||
AutoUpdate=registry |
|||
|
|||
# Network configuration |
|||
Network=host |
|||
|
|||
# No need for root privileges |
|||
User=10027 |
|||
Group=10000 |
|||
|
|||
# Command |
|||
Exec=serve |
|||
|
|||
# Volume mounts |
|||
Volume=/etc/quadlets/ntfy/server.yml:/etc/ntfy/server.yml:ro,z |
|||
Volume=/var/lib/virtiofs/data/ntfy:/var/cache/ntfy:Z |
|||
|
|||
# Health check |
|||
HealthCmd=wget -q --tries=1 http://localhost:8080/v1/health -O - | grep -Eo '"healthy"\s*:\s*true' || exit 1 |
|||
HealthInterval=60s |
|||
HealthTimeout=10s |
|||
HealthStartPeriod=40s |
|||
HealthRetries=3 |
|||
|
|||
[Service] |
|||
Restart=always |
|||
RestartSec=10 |
|||
TimeoutStartSec=120 |
|||
TimeoutStopSec=30 |
|||
|
|||
# Wait for PostgreSQL to be ready on localhost |
|||
ExecStartPre=/bin/sh -c 'exec 2>/dev/null; for try in $(seq 0 12); do if ! /bin/true 5<> /dev/tcp/127.0.0.1/5432; then echo "Waiting for PostgreSQL to be available..."; sleep 5; else exit 0; fi; done; exit 1' |
|||
|
|||
[Install] |
|||
WantedBy=ntfy.target |
|||
@ -0,0 +1,9 @@ |
|||
[Unit] |
|||
Description=podman pull docker.io/binwiederhier/ntfy |
|||
Documentation=https://docs.ntfy.sh/ |
|||
|
|||
# Only start if ntfy has been configured |
|||
ConditionPathExists=/etc/quadlets/ntfy/server.yml |
|||
|
|||
[Image] |
|||
Image=docker.io/binwiederhier/ntfy:v2 |
|||
@ -0,0 +1,13 @@ |
|||
[Unit] |
|||
Description=ntfy Service Target |
|||
Documentation=man:systemd.target(5) |
|||
Requires=postgresql.target ntfy.service |
|||
After=postgresql.target ntfy.service |
|||
|
|||
# Allow isolation - can stop/start this target independently |
|||
AllowIsolate=yes |
|||
# Only start if ntfy has been configured |
|||
ConditionPathExists=/etc/quadlets/ntfy/server.yml |
|||
|
|||
[Install] |
|||
WantedBy=multi-user.target |
|||
@ -0,0 +1,5 @@ |
|||
-- Initialization script for ntfy database and user |
|||
CREATE USER ntfy WITH PASSWORD 'ntfy'; |
|||
CREATE DATABASE ntfy OWNER ntfy; |
|||
GRANT ALL PRIVILEGES ON DATABASE ntfy TO ntfy; |
|||
ALTER ROLE ntfy SET client_encoding TO 'utf8'; |
|||
@ -0,0 +1,12 @@ |
|||
http: |
|||
routers: |
|||
ntfy: |
|||
rule: "Host(`ntfy`)" |
|||
entryPoints: |
|||
- http |
|||
service: "ntfy" |
|||
services: |
|||
ntfy: |
|||
loadBalancer: |
|||
servers: |
|||
- url: "http://127.0.0.1:8080" |
|||
@ -0,0 +1,9 @@ |
|||
variant: fcos |
|||
version: 1.4.0 |
|||
passwd: |
|||
users: |
|||
- name: ntfy |
|||
uid: 10027 |
|||
gecos: ntfy |
|||
home_dir: /var/lib/quadlets/ntfy |
|||
primary_group: itix-svc |
|||
@ -0,0 +1,2 @@ |
|||
d$ /var/lib/virtiofs/data/ntfy 0700 10027 10000 - |
|||
d$ /var/lib/virtiofs/data/ntfy/attachments 0700 10027 10000 - |
|||
@ -0,0 +1,127 @@ |
|||
# Podman Quadlet: Quay Container Registry |
|||
|
|||
## Overview |
|||
|
|||
Quay is a self-hosted container registry started as a Podman Quadlet. It provides image storage, vulnerability scanning, repository mirroring, and proxy caching. |
|||
|
|||
This cookbook runs a complete Quay stack: |
|||
|
|||
- **quay-app**: The main Quay container registry application. |
|||
- **quay-clair**: Clair vulnerability scanner running in combo mode (indexer + matcher + notifier). |
|||
- **quay-init-certificate**: Initializes TLS certificates before Quay starts. |
|||
- **quay-load-renewed-certificate**: Reloads TLS certificates after Lego renewal. |
|||
|
|||
This cookbook uses PostgreSQL as the database backend (requires the `postgresql` cookbook), Redis for caching (requires the `redis` cookbook), and Lego for TLS certificate management (requires the `lego` cookbook). |
|||
|
|||
## Prerequisites |
|||
|
|||
- The `postgresql` cookbook must be installed and running. |
|||
- The `redis` cookbook must be installed and running. |
|||
- The `lego` cookbook must be installed and running. |
|||
- The Quay database and user must be created in PostgreSQL (see `other/postgresql/quay.sh`). |
|||
- The Clair database and user must be created in PostgreSQL (see `other/postgresql/clair.sql`). |
|||
- The Redis ACL for Quay must be configured (see `other/redis/quay.acl`). |
|||
- The nftables firewall rules must be applied (see `other/nftables/50-quay.nft`). |
|||
- Configuration file `/etc/quadlets/quay/app/config.yaml` must exist (see `config/examples/app/config.yaml`). |
|||
- Configuration file `/etc/quadlets/quay/clair/config.yaml` must exist (see `config/examples/clair/config.yaml`). |
|||
|
|||
## Usage |
|||
|
|||
In a separate terminal, follow the logs. |
|||
|
|||
```sh |
|||
sudo make tail-logs |
|||
``` |
|||
|
|||
Install the Podman Quadlets and start Quay. |
|||
|
|||
```sh |
|||
sudo make clean install |
|||
``` |
|||
|
|||
You should see the services starting in order: |
|||
|
|||
1. **quay-init-certificate.service** copies or generates TLS certificates. |
|||
2. **quay-clair.service** starts the Clair vulnerability scanner. |
|||
3. **quay-app.service** starts the Quay registry application. |
|||
|
|||
Access Quay at `https://127.0.0.1:8443/`. |
|||
|
|||
### First login |
|||
|
|||
Bootstrap the initial superuser account: |
|||
|
|||
```sh |
|||
ADMIN_PASSWORD='ChangeMe!' |
|||
curl -vk -X POST https://localhost:8443/api/v1/user/initialize \ |
|||
-H 'Content-Type: application/json' \ |
|||
--data "{\"username\":\"quayadmin\",\"password\":\"${ADMIN_PASSWORD}\",\"email\":\"root@localhost\",\"access_token\":true}" |
|||
``` |
|||
|
|||
## Registry Mirroring |
|||
|
|||
Create a dedicated organization for each registry you want to mirror. For example, to mirror Docker Hub: |
|||
|
|||
 |
|||
|
|||
Then, on your clients, create `/etc/containers/registries.conf.d/mirror.conf` with the following content: |
|||
|
|||
```toml |
|||
[[registry]] |
|||
prefix = "docker.io" |
|||
location = "docker.io" |
|||
|
|||
# Optional: block direct access to the original registry to force clients to use the mirror |
|||
blocked = true |
|||
|
|||
[[registry.mirror]] |
|||
location = "quay.example.test/docker.io" |
|||
|
|||
# Optional: allow insecure access to the mirror if it's using self-signed certificates or plain HTTP |
|||
insecure = true |
|||
``` |
|||
|
|||
You can verify the mirror is working by pulling an image: |
|||
|
|||
```sh |
|||
sudo -i # Open a true session so that credentials are not cleared after each command |
|||
podman login -u quayadmin -p 'ChangeMe!' https://quay.example.test |
|||
podman pull docker.io/library/alpine:latest |
|||
``` |
|||
|
|||
The `docker.io/library/alpine` repository should appear in the Quay UI under the organization you created for Docker Hub. |
|||
The image will be cached in Quay after the first pull. |
|||
|
|||
If you want to access Quay from Podman Quadlets or Systemd units, you need to store the credentials in `/etc/containers/auth.json`: |
|||
|
|||
```sh |
|||
sudo REGISTRY_AUTH_FILE=/etc/containers/auth.json podman login -u quayadmin -p 'ChangeMe!' https://quay.example.test |
|||
``` |
|||
|
|||
And then, in your Quadlet file or Systemd unit, set the environment variable `REGISTRY_AUTH_FILE=/etc/containers/auth.json` so that Podman can find the credentials when pulling images from the mirror. |
|||
|
|||
```ini |
|||
[Service] |
|||
Environment=REGISTRY_AUTH_FILE=/etc/containers/auth.json |
|||
``` |
|||
|
|||
As a regular user, configure the mirror in `~/.config/containers/registries.conf` instead of `/etc/containers/registries.conf`. |
|||
And use the following command to login and store credentials in `~/.config/containers/auth.json`: |
|||
|
|||
```sh |
|||
REGISTRY_AUTH_FILE=~/.config/containers/auth.json podman login -u quayadmin -p 'ChangeMe!' https://quay.example.test |
|||
``` |
|||
|
|||
> [!WARNING] |
|||
> If you enable persistent storage for the credentials, it is safer to generate a read-only robot account in Quay for pulling images from the mirror, and restrict the scope of the credentials to just the mirror repository. This way, if the credentials are leaked, the damage is limited. And if you need to access other parts of the registry read-write, you can login with a regular user account. |
|||
> |
|||
> ``` |
|||
> REGISTRY_AUTH_FILE=~/.config/containers/auth.json podman login -u robot-account -p token https://quay.example.test/docker.io |
|||
> ``` |
|||
> |
|||
> And when needed, you can login with a regular user account for read-write access: |
|||
> |
|||
> ``` |
|||
> podman login -u regular-user -p token https://quay.example.test |
|||
> ``` |
|||
|
|||
@ -1,10 +0,0 @@ |
|||
# Network settings |
|||
port 6379 |
|||
bind 127.0.0.1 |
|||
|
|||
# Set a password for Redis |
|||
requirepass quay |
|||
|
|||
# Hybrid mode (AOF + RDB) |
|||
appendonly yes |
|||
aof-use-rdb-preamble yes |
|||
@ -1 +0,0 @@ |
|||
REDISCLI_AUTH=quay |
|||
@ -0,0 +1,4 @@ |
|||
[Container] |
|||
# Inject a DNS record into /etc/hosts to allow Clair to reach Quay over the loopback interface. |
|||
# TODO: replace "quay" with the FQDN of the Quay instance. |
|||
AddHost=quay:127.0.0.1 |
|||
|
After Width: | Height: | Size: 88 KiB |
Loading…
Reference in new issue