Wiki.js 2.x — Déploiement sur VPS avec Caddy et PostgreSQL¶
Contexte¶
- VPS : Hetzner —
178.105.46.57— domainetekdhi.dev - Caddy : tourne en container Docker (
network_mode: host), gère tous les sous-domaines - PostgreSQL : container
shared-postgressur le réseaushared_infra - Wiki.js installé le 20260517 pour exposer le vault Obsidian de
/home/kanmaber/second-memory/wiki
1. Prérequis vérifiés¶
# Vérifier les containers existants
docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'
# Vérifier le réseau shared_infra
docker inspect shared-postgres | python3 -c "import sys,json; d=json.load(sys.stdin)[0]; nets=d['NetworkSettings']['Networks']; print('Network:', list(nets.keys()))"
# Trouver le Caddyfile (Caddy tourne en Docker, pas systemd)
docker inspect caddy | python3 -c "import sys,json; d=json.load(sys.stdin)[0]; [print(m) for m in d['Mounts']]"
# => Caddyfile monté depuis /home/kanmaber/hermes/Caddyfile
Points critiques découverts :
- Port 3000 déjà pris par open-webui → utiliser 3010 pour Wiki.js
- Caddy tourne avec admin off → pas de caddy reload via API, il faut docker restart caddy
- Le Caddyfile est à /home/kanmaber/hermes/Caddyfile
2. Créer la base PostgreSQL dédiée¶
# Créer user et DB (se connecter avec l'user admin et la DB par défaut)
docker exec shared-postgres psql -U psycho_user -d psychoenLigne -c "CREATE USER wikijs_user WITH PASSWORD 'wikijs_secret2026';"
docker exec shared-postgres psql -U psycho_user -d psychoenLigne -c "CREATE DATABASE wikijs OWNER wikijs_user;"
docker exec shared-postgres psql -U psycho_user -d psychoenLigne -c "GRANT ALL PRIVILEGES ON DATABASE wikijs TO wikijs_user;"
# Vérifier
docker exec shared-postgres psql -U psycho_user -d psychoenLigne -c '\l' | grep wikijs
Credentials PostgreSQL du projet :
- User admin : psycho_user / supersecret123 (dans /home/kanmaber/shared_infra/.env)
- DB par défaut : psychoenLigne (obligatoire dans -d, sinon erreur "database psycho_user does not exist")
3. docker-compose.yml de Wiki.js¶
Fichier : /home/kanmaber/second-memory/docker-compose.yml
services:
wiki:
image: requarks/wiki:2
container_name: wikijs
restart: unless-stopped
ports:
- "127.0.0.1:3010:3000" # port 3000 pris par open-webui → 3010
volumes:
- /home/kanmaber/second-memory/wiki:/home/wiki-content:rw
# ⚠️ JAMAIS monter sur /wiki — c'est le répertoire app de Wiki.js
# Utiliser /home/wiki-content ou tout autre chemin hors /wiki
environment:
- DB_TYPE=postgres
- DB_HOST=shared-postgres # nom du container, résolu par shared_infra
- DB_PORT=5432
- DB_NAME=wikijs
- DB_USER=wikijs_user
- DB_PASS=wikijs_secret2026
networks:
default:
external: true
name: shared_infra # même réseau que shared-postgres pour le résoudre par nom
cd /home/kanmaber/second-memory && docker compose up -d
docker logs --tail 20 wikijs # vérifier "Database Connection Successful"
4. Ajouter wiki.tekdhi.dev au Caddyfile¶
Fichier : /home/kanmaber/hermes/Caddyfile
Bloc à ajouter (Caddy est en network_mode: host → accès direct à 127.0.0.1:3010) :
# --- Wiki.js ---
wiki.tekdhi.dev {
reverse_proxy 127.0.0.1:3010
}
Méthode (python3 pour préserver l'encodage UTF-8) :
python3 - << 'EOF'
path = '/home/kanmaber/hermes/Caddyfile'
with open(path, 'r') as f:
content = f.read()
if 'wiki.tekdhi.dev' not in content:
content += '\n# --- Wiki.js ---\nwiki.tekdhi.dev {\n reverse_proxy 127.0.0.1:3010\n}\n'
with open(path, 'w') as f:
f.write(content)
print('Ajouté')
else:
print('Déjà présent')
EOF
Valider et redémarrer Caddy :
docker exec caddy caddy validate --config /etc/caddy/Caddyfile
docker restart caddy # PAS caddy reload — admin est désactivé (admin off)
docker logs --tail 5 caddy # vérifier "authorization finalized" pour wiki.tekdhi.dev
Caddy obtient le certificat Let's Encrypt automatiquement au premier démarrage.
5. Compléter le setup initial¶
Ouvrir https://wiki.tekdhi.dev dans le navigateur → assistant de configuration :
1. Créer le compte admin (email + mot de passe)
2. Cliquer Install — Wiki.js initialise la DB PostgreSQL
6. Importer les pages Markdown via GraphQL¶
Wiki.js stocke les pages en PostgreSQL. Le module disk est en mode push uniquement (Wiki.js → disque, pas l'inverse). Pour importer des fichiers .md existants, utiliser l'API GraphQL.
Script d'import complet¶
Déposer sur le VPS : /home/kanmaber/import_wiki.py
import subprocess, json, os, re, time, unicodedata
GRAPHQL = 'https://wiki.tekdhi.dev/graphql'
VAULT = '/home/kanmaber/second-memory/wiki' # chemin HÔTE (pas chemin conteneur)
EMAIL = 'bernardkanmdev@gmail.com'
PWD = 'pacome@09052003FENU'
def gql_raw(payload, token=None):
headers = ['-H', 'Content-Type: application/json']
if token:
headers += ['-H', f'Authorization: Bearer {token}']
cmd = ['curl', '-s', '-X', 'POST', GRAPHQL] + headers + ['-d', json.dumps(payload)]
r = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
return json.loads(r.stdout)
def auth():
r = gql_raw({'query': f'mutation {{ authentication {{ login(username: "{EMAIL}", password: "{PWD}", strategy: "local") {{ responseResult {{ succeeded }} jwt }} }} }}'})
return r['data']['authentication']['login']['jwt']
def slugify(text):
text = unicodedata.normalize('NFD', text)
text = ''.join(c for c in text if unicodedata.category(c) != 'Mn')
text = re.sub(r'[^\w\s/-]', '', text)
text = re.sub(r'[\s_]+', '-', text).strip('-')
return text.lower()
TOKEN = auth()
# Désactiver disk storage AVANT import (sinon EACCES au moment de l'écriture)
gql_raw({
'query': 'mutation Dis($targets: [StorageTargetInput]!) { storage { updateTargets(targets: $targets) { responseResult { succeeded } } } }',
'variables': {'targets': [{'key': 'disk', 'isEnabled': False, 'mode': 'push',
'config': [{'key': 'path', 'value': '"/home/wiki-content"'}, {'key': 'createDailyBackups', 'value': 'false'}]}]}
}, TOKEN)
existing_resp = gql_raw({'query': '{ pages { list { path } } }'}, TOKEN)
existing = {p['path'] for p in existing_resp['data']['pages']['list']}
md_files = []
for root, dirs, files in os.walk(VAULT):
dirs[:] = sorted(d for d in dirs if not d.startswith('.'))
for f in sorted(files):
if f.endswith('.md'):
md_files.append(os.path.join(root, f))
CREATE = '''
mutation CreatePage($content: String!, $path: String!, $title: String!, $description: String!) {
pages {
create(content: $content, description: $description, editor: "markdown",
isPrivate: false, isPublished: true, locale: "en",
path: $path, tags: [], title: $title) {
responseResult { succeeded errorCode message }
page { path }
}
}
}
'''
created = skipped = errors = 0
token_refresh_at = time.time() + 25 * 60
for filepath in md_files:
if time.time() > token_refresh_at:
TOKEN = auth()
token_refresh_at = time.time() + 25 * 60
rel = os.path.relpath(filepath, VAULT)
raw_path = rel.replace(os.sep, '/').removesuffix('.md').strip()
wiki_path = '/'.join(slugify(s) for s in raw_path.split('/'))
if wiki_path in existing:
skipped += 1
continue
with open(filepath, 'r', encoding='utf-8', errors='replace') as fh:
content = fh.read()
m = re.search(r'^#\s+(.+)', content, re.MULTILINE)
title = m.group(1).strip() if m else os.path.basename(filepath).removesuffix('.md')
r = gql_raw({'query': CREATE, 'variables': {
'content': content, 'path': wiki_path, 'title': title, 'description': ''
}}, TOKEN)
rr = r.get('data', {}).get('pages', {}).get('create', {}).get('responseResult', {})
if rr.get('succeeded'):
print(f' OK {wiki_path}'); created += 1
else:
print(f' ERR [{rr.get("errorCode")}] {wiki_path}: {rr.get("message")}'); errors += 1
print(f'\nImport: {created} créées, {skipped} ignorées, {errors} erreurs')
# Réactiver disk storage avec chemin corrigé
gql_raw({
'query': 'mutation En($targets: [StorageTargetInput]!) { storage { updateTargets(targets: $targets) { responseResult { succeeded } } } }',
'variables': {'targets': [{'key': 'disk', 'isEnabled': True, 'mode': 'push',
'config': [{'key': 'path', 'value': '"/home/wiki-content"'}, {'key': 'createDailyBackups', 'value': 'false'}]}]}
}, TOKEN)
print('Disk storage réactivé')
python3 /home/kanmaber/import_wiki.py
7. Pièges et solutions documentés¶
Piège 1 — Volume /wiki écrase l'application¶
Wiki.js utilise /wiki comme répertoire racine en interne.
Erreur : Cannot find module '/wiki/server'
Fix : Monter le vault sur /home/wiki-content ou n'importe quel autre chemin ≠ /wiki
Piège 2 — Espace parasite dans le chemin disk storage¶
Le setup wizard ajoute un espace parasite : /home/wiki-content (avec espace).
Erreur : EACCES: permission denied, mkdir '/home/wiki-content '
Fix : Désactiver le disk storage avant d'importer les pages via GraphQL, le réactiver ensuite avec le bon chemin (sans espace) via storage { updateTargets }.
Piège 3 — psql sans -d échoue¶
# ✗ Échoue : "database psycho_user does not exist"
docker exec shared-postgres psql -U psycho_user -c "..."
# ✓ Correct
docker exec shared-postgres psql -U psycho_user -d psychoenLigne -c "..."
Piège 4 — Caddy : caddy reload échoue si admin off¶
# ✗ Échoue : "connection refused" sur port 2019
docker exec caddy caddy reload --config /etc/caddy/Caddyfile
# ✓ Correct
docker restart caddy
Piège 5 — GraphQL : mutation nommée obligatoire avec variables¶
# ✗ Échoue : "Expected $, found :"
mutation($content: String!) { ... }
# ✓ Correct
mutation CreatePage($content: String!) { ... }
Piège 6 — StorageTargetInput : valeurs config en JSON sérialisé¶
# ✗ Échoue : "not valid JSON"
{'key': 'path', 'value': '/home/wiki-content'}
# ✓ Correct : la valeur doit être du JSON sérialisé dans une string
{'key': 'path', 'value': '"/home/wiki-content"'}
Piège 7 — Wiki.js est une SPA : curl montre "Welcome"¶
curl https://wiki.tekdhi.dev retourne <title>Welcome | Wiki.js</title> — c'est le placeholder HTML avant le chargement de Vue.js.
Le vrai contenu est correct, vérifiable via l'API GraphQL :
curl -s -X POST https://wiki.tekdhi.dev/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ pages { single(id:1) { path title content } } }"}'
Piège 8 — disk storage = push uniquement, pas d'import depuis disque¶
Le module disk ne supporte que push (Wiki.js → disque). Il ne peut pas importer des fichiers existants automatiquement. L'import doit se faire via l'API GraphQL (pages { create }).
8. Commandes de maintenance¶
# Voir les logs Wiki.js
docker logs -f wikijs
# Redémarrer Wiki.js
docker compose -f /home/kanmaber/second-memory/docker-compose.yml restart
# Mise à jour Wiki.js
docker pull requarks/wiki:2
docker compose -f /home/kanmaber/second-memory/docker-compose.yml up -d
# Réimporter les nouvelles pages du vault
python3 /home/kanmaber/import_wiki.py
# Vérifier le nombre de pages dans Wiki.js
curl -s -X POST https://wiki.tekdhi.dev/graphql \
-H 'Content-Type: application/json' \
-d '{"query":"{ pages { list { path } } }"}' | python3 -c "import sys,json; d=json.load(sys.stdin); print(len(d['data']['pages']['list']), 'pages')"
9. État final (20260517)¶
| Composant | Détail |
|---|---|
| URL | https://wiki.tekdhi.dev |
| Port interne | 127.0.0.1:3010 |
| DB | PostgreSQL wikijs — user wikijs_user |
| Réseau Docker | shared_infra |
| Vault monté | /home/kanmaber/second-memory/wiki → /home/wiki-content |
| Pages importées | 60 pages |
| Page d'accueil | home (id=1) — contenu de index.md |
| Certificat SSL | Let's Encrypt auto via Caddy |
Piège 9 — Mismatch locale site/pages → page d'accueil affiche "Welcome"¶
Wiki.js configure la langue du site séparément de la locale des pages créées via API.
Symptôme : page d'accueil affiche deux boutons (créer home / admin) malgré une page home existante.
Cause : le site était en lang: fr (setup wizard), les pages créées via GraphQL avec locale: "en".
getPageFromDb filtre sur localeCode = siteLocale → aucune page trouvée → rendu welcome.
Fix : aligner la langue du site sur la locale des pages via PostgreSQL, puis redémarrer :
docker exec shared-postgres psql -U psycho_user -d wikijs -c \\
"UPDATE settings SET value = '{\"code\":\"en\",\"autoUpdate\":true,\"namespacing\":false,\"namespaces\":[\"en\"],\"rtl\":false}' WHERE key = 'lang';"
docker restart wikijs
locale: "fr" dans toutes les mutations GraphQL de création de pages.