mio-server is the self-hostable backend for MioLog.
It provides optional server-side features for the official MioLog PWA:
- sync for local-first data
- optional AI helpers
The official MioLog PWA is currently maintained as the reference client. This repository contains the backend/server project only.
This project is Docker-first. You do not need host-local PHP, Composer, or database extensions installed.
cp .env.example .env
docker compose up -d --build
docker compose exec backend composer install
docker compose exec backend php bin/console doctrine:migrations:migrate
docker compose exec backend php bin/phpunit --testdoxOr use the Makefile wrappers:
make build
make install
make migrate
make testThe API health endpoint is available at:
http://localhost:8000/
Sync works without AI credentials.
To enable AI helpers, configure the provider-specific keys you want to use:
AI_PROVIDER=
GEMINI_API_KEY=
LMSTUDIO_HOST_URL=http://host.docker.internal:1234When AI_PROVIDER is empty, development defaults to lmstudio and non-development
environments default to gemini. All AI helpers use the same provider choice.
AI endpoints accept an optional JSON body such as {"language":"de"} or
{"language":"en"} so generated text can follow the PWA language.
make shell
make console command="debug:router"
make sync-token command="you@example.com iPhone"
make sync-purge-deletions -- --days=180Sync deletion markers are intentionally retained only for a bounded period. Run
app:sync:purge-deletions --days=180 from a daily or weekly cron job. Browsers
that have not synced since before the retained cursor floor recover from the
server-authoritative library on their next sync.
Tagged releases publish production images to GitHub Container Registry:
ghcr.io/thejoekerman/mio-server-backend:<tag>
ghcr.io/thejoekerman/mio-server-web:<tag>
Use latest for the newest published build or pin a release tag such as v1.0.0.
The published production images are intended to run as a small stack:
mio-server-backend: PHP-FPM Symfony appmio-server-web: nginx servingpublic/and forwarding PHP requests tobackend:9000mysql:8.0or another MySQL-compatible database- an outer TLS reverse proxy such as Caddy, nginx, Traefik, or a platform load balancer
The example below creates a complete Docker Compose deployment in /opt/miolog
with Caddy handling HTTPS. Replace miolog-api.example.com, email addresses,
and passwords before running it.
sudo mkdir -p /opt/miolog
sudo chown "$USER":"$USER" /opt/miolog
cd /opt/miologAPP_SECRET_VALUE="$(openssl rand -hex 32)"
cat > .env <<'EOF'
# Public API domain for this backend. Point DNS at this server before starting Caddy.
MIOLOG_DOMAIN=miolog-api.example.com
CADDY_ACME_EMAIL=you@example.com
# Symfony
APP_SECRET=__APP_SECRET__
APP_SHARE_DIR=var/share
DEFAULT_URI=https://miolog-api.example.com
CORS_ALLOW_ORIGIN=^https://app\.miolog\.net$
SYMFONY_TRUSTED_PROXIES=private_ranges
SYMFONY_TRUSTED_HOSTS=^miolog-api\.example\.com$
# MySQL
MYSQL_ROOT_PASSWORD=replace-with-a-root-password
MYSQL_DATABASE=miolog
MYSQL_USER=miolog
MYSQL_PASSWORD=replace-with-an-app-password
# Optional AI settings
AI_PROVIDER=
GEMINI_API_KEY=
LMSTUDIO_HOST_URL=
# mio-server images
MIOLOG_BACKEND_IMAGE=ghcr.io/thejoekerman/mio-server-backend:latest
MIOLOG_WEB_IMAGE=ghcr.io/thejoekerman/mio-server-web:latest
EOF
sed -i "s/__APP_SECRET__/${APP_SECRET_VALUE}/" .envcat > compose.yml <<'EOF'
name: miolog
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
environment:
MIOLOG_DOMAIN: ${MIOLOG_DOMAIN:?Set MIOLOG_DOMAIN in .env}
CADDY_ACME_EMAIL: ${CADDY_ACME_EMAIL:?Set CADDY_ACME_EMAIL in .env}
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
web:
condition: service_healthy
networks:
- public
- app
web:
image: ${MIOLOG_WEB_IMAGE:?Set MIOLOG_WEB_IMAGE in .env}
restart: unless-stopped
depends_on:
backend:
condition: service_started
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1/healthz >/dev/null"]
interval: 30s
timeout: 5s
retries: 3
networks:
- app
backend:
image: ${MIOLOG_BACKEND_IMAGE:?Set MIOLOG_BACKEND_IMAGE in .env}
restart: unless-stopped
environment:
APP_ENV: prod
APP_DEBUG: "0"
APP_SECRET: ${APP_SECRET:?Set APP_SECRET in .env}
APP_SHARE_DIR: ${APP_SHARE_DIR:-var/share}
DEFAULT_URI: ${DEFAULT_URI:?Set DEFAULT_URI in .env}
DATABASE_URL: mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@db:3306/${MYSQL_DATABASE}?serverVersion=8.0.32&charset=utf8mb4
MESSENGER_TRANSPORT_DSN: ${MESSENGER_TRANSPORT_DSN:-doctrine://default?auto_setup=0}
CORS_ALLOW_ORIGIN: ${CORS_ALLOW_ORIGIN:?Set CORS_ALLOW_ORIGIN in .env}
SYMFONY_TRUSTED_PROXIES: ${SYMFONY_TRUSTED_PROXIES:-private_ranges}
SYMFONY_TRUSTED_HOSTS: ${SYMFONY_TRUSTED_HOSTS:?Set SYMFONY_TRUSTED_HOSTS in .env}
AI_PROVIDER: ${AI_PROVIDER:-}
GEMINI_API_KEY: ${GEMINI_API_KEY:-}
LMSTUDIO_HOST_URL: ${LMSTUDIO_HOST_URL:-}
depends_on:
db:
condition: service_healthy
networks:
- app
- egress
db:
image: mysql:8.0
restart: unless-stopped
ports:
- "127.0.0.1:3306:3306"
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?Set MYSQL_ROOT_PASSWORD in .env}
MYSQL_DATABASE: ${MYSQL_DATABASE:?Set MYSQL_DATABASE in .env}
MYSQL_USER: ${MYSQL_USER:?Set MYSQL_USER in .env}
MYSQL_PASSWORD: ${MYSQL_PASSWORD:?Set MYSQL_PASSWORD in .env}
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
volumes:
- db_data:/var/lib/mysql
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h 127.0.0.1 -u$${MYSQL_USER} -p$${MYSQL_PASSWORD} --silent"]
interval: 10s
timeout: 5s
retries: 10
networks:
- app
volumes:
caddy_data:
caddy_config:
db_data:
networks:
public:
egress:
app:
internal: true
EOFcat > Caddyfile <<'EOF'
{
email {$CADDY_ACME_EMAIL}
}
{$MIOLOG_DOMAIN} {
encode zstd gzip
reverse_proxy web:80
}
EOFdocker compose -f compose.yml pull
docker compose -f compose.yml up -d
docker compose -f compose.yml exec -T backend php bin/console doctrine:migrations:migrate --no-interactionSchedule deletion-marker cleanup on the host, for example:
15 3 * * * cd /opt/miolog && docker compose -f compose.yml exec -T backend php bin/console app:sync:purge-deletions --days=180Create a sync token for the PWA:
docker compose -f compose.yml exec -T backend php bin/console app:sync-token:create you@example.com "My device"Then open the hosted MioLog PWA at https://app.miolog.net, set the sync API
base URL to your API domain, and paste the token.
MIT.
