Setting it up with Docker
The same parts as without Docker, packaged. The files are in server/docker/prod/ of the repository. No image is published in any registry: you build it yourself, on your machine, from the code of the version you choose. The Dockerfiles in server/docker/ (without prod) are only for development: do not use them on a server.
What this setup does:
- The PHP image has no debugger and no development dependencies, runs as an unprivileged user (
www-data) and carries the code inside (nothing is mounted). - Four services:
php(PHP-FPM),nginx,db(PostgreSQL 18, with its volume) andworker(the scheduled tasks, with automatic restart). Mercure is optional. - nginx listens on HTTP, on a local port (by default
127.0.0.1:8080). HTTPS is terminated by a proxy in front, the one the machine already has or an nginx with Certbot. The configuration does not include certificates. - PostgreSQL publishes no port.
You need Docker with the Compose plugin (version 2).
1. The code and the configuration
Section titled “1. The code and the configuration”git clone https://github.com/kosmotheka/kosmotheka.git /srv/kosmothekacd /srv/kosmothekagit checkout vX.Y.Z # the version you wantcd server/docker/prodcp env.example .envchmod 600 .envFill in .env (all the secrets are generated with openssl, as the file itself says):
POSTGRES_PASSWORD:openssl rand -hex 24.APP_SECRET:openssl rand -hex 32.CLOUD_KEY_SECRET:openssl rand -base64 32(base64, not hexadecimal).DEFAULT_URIandINVITATION_BASE_URL:https://your-domain, without a trailing slash.HTTP_PORTandHTTP_BIND, if the local 8080 is busy.
The rest of the variables (email, Google, search services…) are added to the same .env: they reach the PHP containers as they are. Each one is in the configuration. Keep a copy of .env outside the machine: without CLOUD_KEY_SECRET the copy of the database is no use for signing.
2. Build and start
Section titled “2. Build and start”docker compose builddocker compose up -ddocker compose run --rm php bin/console doctrine:migrations:migrate --no-interactionThe first build takes a few minutes. Check that everything is running:
docker compose pscurl -s http://127.0.0.1:8080/v1/serverIt must answer with the server’s JSON. To have several servers on the same machine (or a test one next to it), change COMPOSE_PROJECT_NAME and the port in .env.
3. HTTPS: the proxy in front
Section titled “3. HTTPS: the proxy in front”Any proxy with TLS works; this is an nginx with Certbot on the machine itself (outside Docker):
server { listen 80; server_name your-domain;
location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300; client_max_body_size 16m; }
# These two addresses carry a secret or what an account searches for: # they are not written to this proxy's access log. location ~ ^/v1/(auth/google/(connect|callback)|metadata/) { access_log off; proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; }}sudo nginx -t && sudo systemctl reload nginxsudo certbot --nginx -d your-domaincurl -s https://your-domain/v1/serverThe nginx inside the container already leaves those addresses out of its own log. If you use another proxy or a CDN, they must not log them either. The TRUSTED_PROXIES variable already comes with REMOTE_ADDR to trust what that proxy sends; do not open the container’s port to the internet (that is why it listens only on 127.0.0.1).
4. Instant notices (optional)
Section titled “4. Instant notices (optional)”Generate two different keys with openssl rand -hex 32, put them in .env next to MERCURE_URL=http://mercure/.well-known/mercure:
MERCURE_URL=http://mercure/.well-known/mercureMERCURE_PUBLISHER_JWT_KEY=...MERCURE_SUBSCRIBER_JWT_KEY=...and start with the profile: docker compose --profile mercure up -d. nginx already forwards /.well-known/mercure to the hub. For the proxy in front, add the same location /.well-known/mercure as in the guide without Docker, pointing to the local port, without buffering and with a long read timeout.
Everything goes to the output of each container: docker compose logs -f php nginx worker.
Copies, updating and problems
Section titled “Copies, updating and problems”- Copy: the dump comes out of the database container and is encrypted on the fly, as in the backups:
docker compose exec -T db pg_dump -U kosmotheka --format=custom kosmotheka | age -R /etc/kosmotheka/backup-recipient.txt > db.dump.age, and the same for.envinstead of.env.local. To restore onto an empty database:age -d -i clave.txt db.dump.age | docker compose exec -T db pg_restore -U kosmotheka --no-owner --dbname=kosmotheka. (The Docker part of these two commands has not been tested; the encryption andpg_restorehave, see the backups.) - Updating: copy;
git checkoutof the new version;docker compose build;docker compose run --rm php bin/console doctrine:migrations:migrate --no-interaction;docker compose up -d. The cache is already prepared in the image. What is said in updating still applies: first the app in the stores, if the notes ask for it. - A Symfony command:
docker compose run --rm php bin/console …. - Emptying and starting from zero:
docker compose down -vdeletes the containers and the database. It is irreversible.
© 2026 Kosmotheka
Kosmotheka is free software: the app and the server are under the AGPL-3.0-or-later, and the operation spec is under the MIT licence. The name and the logo are not covered by the licence.
kosmotheka.com · Trademark policy · Privacy policy · info@kosmotheka.com