Merge pull request #53 from IliesAllali/docker/aio

Docker : image tout-en-un pour Unraid et les NAS
This commit is contained in:
IliesAllali authored and GitHub committed 2026-10-09 17:53:04 +02:00
commit b774b1dff8
8 files changed
+291

No files matched your search

+41
View File
@@ -191,3 +191,44 @@ jobs:
- name: Arrêt et suppression des volumes
if: always()
run: docker compose down -v --remove-orphans
docker-aio:
name: Docker tout-en-un (Unraid, NAS)
runs-on: ubuntu-latest
timeout-minutes: 30
env:
SMOKE_WAIT_SECONDS: 180
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
persist-credentials: false
- name: Construction
run: DOCKER_BUILDKIT=1 docker build -f Dockerfile.aio -t parenthese-aio:test .
- name: Démarrage, sans aucune variable
run: docker run -d --name aio -p 8080:80 -v aio-data:/data parenthese-aio:test
- name: Test de fumée par le port web
run: bash tools/docker-smoke.sh http://localhost:8080
# Les secrets et la base de /data doivent servir tels quels au démarrage suivant (mise à jour, reboot).
- name: Redémarrage puis nouveau test
run: |
set -euo pipefail
docker restart aio
bash tools/docker-smoke.sh http://localhost:8080
docker logs aio 2>&1 | grep -c "première installation" | grep -qx 1
- name: Journaux en cas d'échec
if: failure()
run: |
docker ps -a
docker logs --timestamps aio || true
docker run --rm -v aio-data:/data busybox cat /data/postgres/postgres.log || true
- name: Arrêt et suppression du volume
if: always()
run: |
docker rm -f aio || true
docker volume rm aio-data || true
+4
View File
@@ -53,6 +53,10 @@ jobs:
- image: parenthese-web
context: .
file: Dockerfile.web
# Tout-en-un (base, API, front) pour Unraid et les NAS
- image: parenthese-aio
context: .
file: Dockerfile.aio
steps:
# Sur workflow_run, le commit exact que la CI a validé, pas la tête de main au moment du run.
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
+6
View File
@@ -2,6 +2,12 @@
Notable changes to Parenthèse. Dates are in YYYY-MM-DD format.
## Unreleased
### Self-hosting
- Single-container image `ghcr.io/iliesallali/parenthese-aio` (PostgreSQL, API and front end), for Unraid and NAS systems that install one container per app. It needs no configuration: the database, its password and the session key are created in `/data` on first start. Tested in CI by a fresh start, a restart and the smoke test.
## 1.0.2 (2026-10-09)
### Self-hosting
+94
View File
@@ -0,0 +1,94 @@
# syntax=docker/dockerfile:1
# Parenthèse en un seul conteneur : PostgreSQL, API et front servi par nginx. Contexte de build : la racine du dépôt.
# Pour les systèmes qui installent une application par conteneur (Unraid, NAS). docker-compose.yml reste
# l'installation de référence, avec un conteneur par service.
#
# docker run -d --name parenthese -p 8080:80 -v parenthese-data:/data ghcr.io/iliesallali/parenthese-aio
#
# Tout ce qui doit survivre à une mise à jour vit dans /data : base (postgres/), photos et vidéos (media/),
# secrets générés au premier démarrage (secrets/). Voir deploy/aio/entrypoint.sh.
# Étape 1 : API (mêmes étapes que backend/Dockerfile).
FROM node:24-bookworm-slim AS api-build
RUN apt-get update \
&& apt-get install -y --no-install-recommends openssl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY backend/package.json backend/package-lock.json ./
COPY backend/prisma ./prisma
RUN npm ci --no-audit --no-fund
RUN npx prisma generate
COPY backend/tsconfig.json ./
COPY backend/src ./src
RUN npm run build
# Étape 2 : front (mêmes étapes que Dockerfile.web).
FROM node:24-alpine AS web-build
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json ./
RUN npm ci
COPY frontend/ ./
ENV VITE_API_BASE_URL=""
RUN npm run build
# Étape 3 : image finale.
FROM node:24-bookworm-slim
# postgresql-common d'abord, pour qu'il ne crée pas de base par défaut à l'installation de PostgreSQL :
# la base vit dans /data et naît au premier démarrage.
RUN apt-get update \
&& apt-get install -y --no-install-recommends postgresql-common \
&& sed -i 's/^#\?\s*create_main_cluster.*/create_main_cluster = false/' /etc/postgresql-common/createcluster.conf \
&& apt-get install -y --no-install-recommends postgresql-15 nginx openssl ca-certificates tini \
&& rm -rf /var/lib/apt/lists/* \
&& rm -f /etc/nginx/sites-enabled/default \
&& mkdir -p /run/postgresql && chown postgres:postgres /run/postgresql
WORKDIR /app
COPY backend/package.json backend/package-lock.json ./
RUN npm ci --omit=dev --no-audit --no-fund \
&& npm cache clean --force
# CLI Prisma à part, à la version du package-lock (voir backend/Dockerfile).
RUN PRISMA_VERSION="$(node -p "require('./package-lock.json').packages['node_modules/prisma'].version")" \
&& mkdir -p /opt/prisma \
&& cd /opt/prisma \
&& echo '{"private":true}' > package.json \
&& npm install --no-audit --no-fund "prisma@${PRISMA_VERSION}" \
&& npm cache clean --force
COPY --from=api-build /app/node_modules/.prisma ./node_modules/.prisma
COPY backend/prisma ./prisma
COPY --from=api-build /app/dist ./dist
# nginx : la configuration du conteneur web de compose, l'API étant ici sur la même machine.
COPY deploy/nginx.docker.conf /etc/nginx/conf.d/default.conf
RUN sed -i 's|http://api:4000|http://127.0.0.1:4000|g' /etc/nginx/conf.d/default.conf \
&& ! grep -q 'api:4000' /etc/nginx/conf.d/default.conf
COPY --from=web-build /app/dist /usr/share/nginx/html
COPY deploy/aio/entrypoint.sh /usr/local/bin/parenthese-aio
RUN chmod 755 /usr/local/bin/parenthese-aio
ENV NODE_ENV=production \
CHECKPOINT_DISABLE=1 \
PATH=/opt/prisma/node_modules/.bin:/usr/lib/postgresql/15/bin:$PATH
VOLUME /data
EXPOSE 80
HEALTHCHECK --interval=15s --timeout=5s --start-period=60s --retries=5 \
CMD node -e "fetch('http://127.0.0.1/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
ENTRYPOINT ["tini", "--", "/usr/local/bin/parenthese-aio"]
+20
View File
@@ -0,0 +1,20 @@
# Contexte de build de Dockerfile.aio (BuildKit lit ce fichier à la place de .dockerignore) :
# l'API, le front, la configuration nginx et le script de démarrage
*
!backend
!frontend
!deploy/nginx.docker.conf
!deploy/aio
backend/node_modules
backend/dist
backend/.local
backend/storage
backend/test
backend/openapi
backend/.env
backend/.env.*
frontend/node_modules
frontend/dist
frontend/.env
frontend/.env.*
**/*.log
+10
View File
@@ -71,6 +71,16 @@ docker compose down # arrêt, les données sont conservées
Pour vérifier qu'une installation fonctionne de bout en bout, lancez `bash tools/docker-smoke.sh http://localhost` (avec le port si `WEB_PORT` n'est pas 80). Le script a besoin de `curl` et `jq`. Il passe par le port web comme un navigateur : page d'accueil, santé de l'API, création d'un compte, connexion, création puis suppression d'un arbre de test. Le compte de test (`smoke-...@example.com`) reste en base.
### Un seul conteneur (Unraid, NAS)
Pour les systèmes qui installent un conteneur par application, `ghcr.io/iliesallali/parenthese-aio` fait tourner la base, l'API et le front ensemble :
```bash
docker run -d --name parenthese --restart unless-stopped -p 8080:80 -v /chemin/vers/appdata/parenthese:/data ghcr.io/iliesallali/parenthese-aio
```
Rien à remplir. Au premier démarrage, le conteneur crée dans `/data` la base, son mot de passe et la clé des sessions, puis les réutilise. Les photos et vidéos vont dans `/data/media`. Sauvegardez tout le dossier `/data` conteneur arrêté, ou exportez la base pendant qu'il tourne avec `docker exec parenthese runuser -u postgres -- pg_dump parenthese > parenthese.sql`. La base est ici PostgreSQL 15, l'installation compose utilise la 16.
### Où sont les données
Deux volumes Docker, conservés entre les redémarrages et les mises à jour :
+10
View File
@@ -77,6 +77,16 @@ docker compose down # stop, data is kept
To check that an installation works end to end, run `bash tools/docker-smoke.sh http://localhost` (add the port if `WEB_PORT` is not 80). The script needs `curl` and `jq`. It goes through the web port like a browser: home page, API health, account creation, sign-in, creating then deleting a test tree. The test account (`smoke-...@example.com`) stays in the database.
### Single container (Unraid, NAS)
For systems that install one container per app, `ghcr.io/iliesallali/parenthese-aio` runs the database, the API and the front end together:
```bash
docker run -d --name parenthese --restart unless-stopped -p 8080:80 -v /path/to/appdata/parenthese:/data ghcr.io/iliesallali/parenthese-aio
```
There is nothing to fill in. On first start the container creates the database, its password and the session key in `/data`, and reuses them afterwards. Photos and videos go to `/data/media`. Back up the whole `/data` folder with the container stopped, or dump the database while it runs with `docker exec parenthese runuser -u postgres -- pg_dump parenthese > parenthese.sql`. The database is PostgreSQL 15 here, the compose setup uses 16.
### Where the data lives
Two Docker volumes, kept across restarts and updates:
+106
View File
@@ -0,0 +1,106 @@
#!/bin/bash
# Démarrage de l'image tout-en-un (Dockerfile.aio) : PostgreSQL, puis l'API, puis nginx.
#
# Au premier démarrage, crée dans /data la base, le mot de passe de la base et la clé de signature des sessions
# (JWT_SECRET), gardés pour les démarrages suivants. Une variable JWT_SECRET passée au conteneur l'emporte sur
# la clé générée. Si l'un des trois processus s'arrête, le conteneur s'arrête, et le redémarrage automatique
# de Docker le relance.
set -euo pipefail
DATA_DIR=/data
PGDATA="$DATA_DIR/postgres"
MEDIA_DIR="$DATA_DIR/media"
SECRETS_DIR="$DATA_DIR/secrets"
DB_NAME=parenthese
DB_USER=parenthese
log() { echo "[parenthese] $*" >&2; }
mkdir -p "$MEDIA_DIR" "$SECRETS_DIR" /run/postgresql
chmod 700 "$SECRETS_DIR"
chown postgres:postgres /run/postgresql
chown node:node "$MEDIA_DIR"
# Secrets générés une fois, lisibles par root seulement.
secret() {
local file="$SECRETS_DIR/$1"
if [ ! -s "$file" ]; then
(umask 077 && openssl rand -hex "$2" > "$file")
log "secret $1 créé dans $file"
fi
cat "$file"
}
DB_PASSWORD="$(secret db_password 24)"
if [ -z "${JWT_SECRET:-}" ]; then
JWT_SECRET="$(secret jwt_secret 48)"
fi
# PostgreSQL, à l'écoute de la machine seule.
FIRST_START=0
if [ ! -s "$PGDATA/PG_VERSION" ]; then
log "première installation : création de la base dans $PGDATA"
mkdir -p "$PGDATA"
chown postgres:postgres "$PGDATA"
chmod 700 "$PGDATA"
runuser -u postgres -- initdb -D "$PGDATA" --encoding=UTF8 --locale=C.UTF-8 \
--auth-local=peer --auth-host=scram-sha-256 >/dev/null
FIRST_START=1
fi
chown -R postgres:postgres "$PGDATA"
chmod 700 "$PGDATA"
runuser -u postgres -- pg_ctl -D "$PGDATA" -w -t 60 \
-o "-c listen_addresses=127.0.0.1 -c unix_socket_directories=/run/postgresql" \
-l "$PGDATA/postgres.log" start >/dev/null
if [ "$FIRST_START" = 1 ]; then
runuser -u postgres -- psql -q -v ON_ERROR_STOP=1 -c "CREATE ROLE $DB_USER LOGIN"
runuser -u postgres -- createdb -O "$DB_USER" "$DB_NAME"
fi
# Le mot de passe suit le fichier de /data à chaque démarrage (hexadécimal, sans guillemet possible).
runuser -u postgres -- psql -q -v ON_ERROR_STOP=1 -c "ALTER ROLE $DB_USER PASSWORD '$DB_PASSWORD'"
export NODE_ENV=production
export PORT=4000
export HOST=127.0.0.1
export DATABASE_URL="postgresql://$DB_USER:$DB_PASSWORD@127.0.0.1:5432/$DB_NAME"
export MEDIA_STORAGE_PATH="$MEDIA_DIR"
export JWT_SECRET
stop_all() {
log "arrêt"
[ -n "${NGINX_PID:-}" ] && kill -QUIT "$NGINX_PID" 2>/dev/null || true
[ -n "${API_PID:-}" ] && kill -TERM "$API_PID" 2>/dev/null || true
wait 2>/dev/null || true
runuser -u postgres -- pg_ctl -D "$PGDATA" -m fast -w stop >/dev/null 2>&1 || true
}
trap 'stop_all; exit 0' TERM INT
cd /app
log "migrations"
runuser -u node -- env PATH="$PATH" prisma migrate deploy
log "démarrage de l'API"
runuser -u node -- env PATH="$PATH" node dist/server.js &
API_PID=$!
for _ in $(seq 1 60); do
if node -e "fetch('http://127.0.0.1:4000/health').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"; then
break
fi
kill -0 "$API_PID" 2>/dev/null || { log "l'API s'est arrêtée au démarrage"; stop_all; exit 1; }
sleep 1
done
log "démarrage de nginx"
nginx -g 'daemon off;' &
NGINX_PID=$!
log "prêt sur le port 80"
set +e
wait -n "$API_PID" "$NGINX_PID"
status=$?
log "un processus s'est arrêté (code $status)"
stop_all
exit "$status"