Setting it up without Docker
This is how the official server works. The steps assume a recent Debian or Ubuntu machine with administrator access; on another distribution the package names and paths change, not the ideas. In the examples, your-domain is the name of your server and /srv/kosmotheka the place where you put the code: change them for yours.
1. What to install
Section titled “1. What to install”| Part | Version | What for |
|---|---|---|
| PHP | 8.5 or later, with PHP-FPM | The server is a Symfony 8 application |
| PostgreSQL | 18 | The database. Doctrine generates SQL for the version it is told (serverVersion=18) |
| nginx | any recent one | Serves HTTPS and passes the requests to PHP-FPM |
| Composer | 2 | Installs the PHP dependencies |
| Certbot | any | The HTTPS certificate (Let’s Encrypt) |
PHP extensions: pdo_pgsql, sodium (the signatures and the encryption of the cloud keys), mbstring, intl, xml (includes simplexml), curl, ctype, iconv and opcache. The PHP and PostgreSQL versions that come with the distributions are usually older: use the official PostgreSQL repository and, for PHP, one that carries 8.5 (on Debian and Ubuntu, the one by Ondřej Surý).
If several PHP versions live on the machine, call php8.5 explicitly in all the commands in this guide (php8.5 bin/console …, php8.5 /usr/bin/composer …). A bare php may be another version and fail in confusing ways.
2. The database
Section titled “2. The database”Its own user and database, only for Kosmotheka:
sudo -u postgres psql <<'SQL'CREATE ROLE kosmotheka LOGIN PASSWORD 'put-a-long-password-here';CREATE DATABASE kosmotheka OWNER kosmotheka ENCODING 'UTF8';SQLGenerate the password with openssl rand -hex 24 instead of making one up. PostgreSQL must listen only on 127.0.0.1 (it does so by default): do not open its port to the internet.
3. The code
Section titled “3. The code”sudo mkdir -p /srv/kosmotheka && sudo chown "$USER": /srv/kosmothekagit clone https://github.com/kosmotheka/kosmotheka.git /srv/kosmothekacd /srv/kosmothekagit checkout vX.Y.Z # the tag of the version you want; see the published versionsThe server is the server/ folder. It needs the spec/ folder next to it (the format of the operations, which it reads on startup), so clone the whole repository and do not move one folder without the other.
4. The configuration: .env.local
Section titled “4. The configuration: .env.local”All the configuration goes in server/.env.local, a file that is not in git. Create a minimal one (all the variables, one by one, are in the configuration):
cd /srv/kosmotheka/serverumask 077cat > .env.local <<ENVAPP_ENV=prodAPP_SECRET=$(openssl rand -hex 32)DATABASE_URL="postgresql://kosmotheka:THE_PASSWORD@127.0.0.1:5432/kosmotheka?serverVersion=18&charset=utf8"DEFAULT_URI=https://your-domainINVITATION_BASE_URL=https://your-domainCLOUD_KEY_SECRET=$(openssl rand -base64 32)TRUSTED_PROXIES=127.0.0.1ENVchmod 640 .env.localCLOUD_KEY_SECRETis in base64 (openssl rand -base64 32), not hexadecimal.- Keep a copy of
.env.localoutside the machine. WithoutCLOUD_KEY_SECRET, a copy of the database does not let the server sign for the libraries already created. DEFAULT_URIis the public address, without a trailing slash, and it is the same one you will write in the app. Changing it is, for the apps, changing server.- For email, Google and the search services to work, add their variables afterwards (see the configuration).
5. Dependencies and migrations
Section titled “5. Dependencies and migrations”cd /srv/kosmotheka/serverphp8.5 /usr/bin/composer install --no-dev --optimize-autoloaderphp8.5 bin/console doctrine:migrations:migrate --no-interactionphp8.5 bin/console cache:clearThe migrations create all the tables. If one fails, do not go on: read the error (almost always it is the connection to the database or a missing extension).
PHP-FPM must be able to write to server/var/ (the cache and the logs). Give that folder to the user that runs the pool (on Debian and Ubuntu, www-data):
sudo chown -R www-data: /srv/kosmotheka/server/var6. PHP-FPM
Section titled “6. PHP-FPM”A pool of its own keeps Kosmotheka’s memory separate from everything else. In /etc/php/8.5/fpm/pool.d/kosmotheka.conf:
[kosmotheka]user = www-datagroup = www-datalisten = /run/php/php8.5-fpm-kosmotheka.socklisten.owner = www-datalisten.group = www-datapm = ondemandpm.max_children = 8pm.process_idle_timeout = 30sphp_admin_value[memory_limit] = 256MOn a small machine, a low pm.max_children stops a spike from using up the memory. Restart: sudo systemctl restart php8.5-fpm.
7. nginx
Section titled “7. nginx”Two files. The first goes outside the site, in /etc/nginx/conf.d/kosmotheka-log.conf, because map only works at the http level:
# Which addresses are never written to the access log:# Google's return address carries a secret, and /v1/metadata/ carries what an account searches for.map $request_uri $kosmotheka_loggable { ~^/v1/auth/google/(connect|callback)(\?|$) 0; ~^/v1/metadata/ 0; default 1;}The second is the site, in /etc/nginx/sites-available/kosmotheka (link it in sites-enabled):
server { listen 80; server_name your-domain; root /srv/kosmotheka/server/public;
# Instant notices (optional, see below). If you do not install Mercure, remove this block. # location /.well-known/mercure { # proxy_pass http://127.0.0.1:3000; # proxy_http_version 1.1; # proxy_set_header Connection ""; # proxy_set_header Host $host; # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # proxy_set_header X-Forwarded-Proto $scheme; # proxy_buffering off; # proxy_cache off; # proxy_read_timeout 24h; # }
# It is an API: everything goes to Symfony. location / { try_files $uri /index.php$is_args$args; }
location ~ ^/index\.php(/|$) { fastcgi_pass unix:/run/php/php8.5-fpm-kosmotheka.sock; fastcgi_split_path_info ^(.+\.php)(/.*)$; include fastcgi_params; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; fastcgi_param DOCUMENT_ROOT $document_root; internal; }
location ~ \.php$ { return 404; }
client_max_body_size 16m; access_log /var/log/nginx/kosmotheka_access.log combined if=$kosmotheka_loggable; error_log /var/log/nginx/kosmotheka_error.log warn;}If there is a proxy or a CDN in front of nginx, it must not log those addresses either. Check and reload:
sudo nginx -t && sudo systemctl reload nginx8. HTTPS with Certbot
Section titled “8. HTTPS with Certbot”With the name already pointing to the machine and port 80 open:
sudo certbot --nginx -d your-domainCertbot adds the certificate, redirects port 80 to 443 and renews by itself (check that its timer exists: systemctl list-timers | grep certbot). Open in the firewall only 80 and 443 (and SSH).
Test the server from outside:
curl -s https://your-domain/v1/serverIt must answer with a JSON with what the server offers (invitations, events, sign_in_with_google…). If it answers, you can write https://your-domain under “Other server” in the app.
9. The scheduled-task process
Section titled “9. The scheduled-task process”The server has a task every hour (it deletes unconfirmed accounts and expired codes) that does nothing by itself: it only runs while something consumes its queue. It is a systemd service, in /etc/systemd/system/kosmotheka-worker.service:
[Unit]Description=Kosmotheka scheduled tasksAfter=network.target postgresql.service
[Service]User=www-dataWorkingDirectory=/srv/kosmotheka/serverExecStart=/usr/bin/php8.5 bin/console messenger:consume scheduler_default --time-limit=3600 --memory-limit=128MRestart=alwaysRestartSec=5
[Install]WantedBy=multi-user.targetsudo systemctl daemon-reloadsudo systemctl enable --now kosmotheka-workersystemctl status kosmotheka-workerThe --time-limit makes it restart itself every hour, with clean memory; Restart=always brings it back up. If you do not want the service, you can run the purge by hand from time to time: php8.5 bin/console app:accounts:purge.
10. Instant notices (optional)
Section titled “10. Instant notices (optional)”Mercure is a separate component that lets open apps know that there is something new. Without it everything works the same: the apps update when they are opened. The server says so in /v1/server (events: false).
If you want it, install the Mercure hub (version 1.0) as a systemd service, listening only on 127.0.0.1:3000, and:
- Generate two different keys:
openssl rand -hex 32for the publisher (only the server uses it) and another one for the subscriber (it signs the tokens the apps receive). One cannot publish with the other. - In the hub: both keys with the HS256 algorithm,
MERCURE_TRUSTED_ISSUERS=urn:kosmotheka:server, and the directiveresource_identifier http://127.0.0.1:3000/.well-known/mercure(the audience of the tokens, fixed). - In
.env.local:MERCURE_URL=http://127.0.0.1:3000/.well-known/mercure(the same address asresource_identifier),MERCURE_PUBLISHER_JWT_KEYandMERCURE_SUBSCRIBER_JWT_KEY. - Remove the comments from the
/.well-known/mercureblock in nginx. php8.5 bin/console cache:clearand restart PHP-FPM.
To confirm: this part is taken from how development works (where the hub runs in a container) and has not been tested with the hub installed as a service. If you do not need it, skip it.
Afterwards
Section titled “Afterwards”With this the server already works. What is left is the backups, which are not optional, and configuring the email, Google and the search services according to what you want to offer.
© 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