mirror of
https://github.com/IliesAllali/parenthese.git
synced 2026-11-06 23:32:08 +00:00
Merge pull request #53 from IliesAllali/docker/aio
Docker : image tout-en-un pour Unraid et les NAS
This commit is contained in:
8 files changed
+291
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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 :
|
||||
|
||||
@@ -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:
|
||||
|
||||
Executable
+106
@@ -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"
|
||||
Reference in new issue
Block a user