Quick start with Docker Compose
Peristyle fits in one Docker image, next to PostgreSQL. You need Docker with Docker Compose on an amd64 machine, and the docker-compose.yml and .env.example files of the repository on the server: the image is downloaded from ghcr.io/zeguym/peristyle.
-
Prepare the configuration
Copy the sample file, then replace
POSTGRES_PASSWORDwith a real password.cp .env.example .env -
Start
Docker Compose downloads the image and starts Peristyle and PostgreSQL.
docker compose up -d -
Create the administrator account
Open
http://<server>:8085and create an account: the first account administers the instance. Then create the other accounts, or send invitations. -
Close registration
Once everyone is in, turn off Open registration in Administration → Settings, or set
ALLOW_REGISTRATION=falsein.envand rundocker compose up -dagain.
Change the port with PERISTYLE_PORT in .env.
Updating
Back up the instance first, then download the new image. The database is updated at startup.
docker compose pull
docker compose up -d
PERISTYLE_VERSION in .env selects the image: latest by default, a version (1.2.3) or a minor line (1.2).
Building from the sources
With a full copy of the repository:
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
Using an existing PostgreSQL server
Remove the db service from docker-compose.yml and point ConnectionStrings__peristyle to your server, with a dedicated database and user.
Synology NAS
Installation goes through Container Manager and the same docker-compose.yml file.
-
Copy the files
Copy
docker-compose.ymland.env.exampleto a folder on the NAS, then.env.exampleto.env, and changePOSTGRES_PASSWORD. -
Create the project
In Container Manager → Project → Create, point to that folder. You can also run
docker compose up -dover SSH. -
Create the accounts
Open
http://<nas-ip>:8085, create the administrator account, then the family accounts. -
Close registration
Set
ALLOW_REGISTRATION=falsein.envand start the project again, or use Administration → Settings.
Reverse proxy
To publish the instance over HTTPS, put it behind a reverse proxy. On a Synology NAS, open Control Panel → Login Portal → Advanced → Reverse Proxy:
- source:
https://peristyle.example.com; - destination:
http://localhost:8085; - custom headers: turn on
WebSocket.
Peristyle only trusts X-Forwarded-* headers from the addresses listed in TRUSTED_PROXIES (addresses or CIDR networks, comma separated). When empty, the whole local network is accepted.
Backups
A full backup has two parts: the database and the /data volume. Keep your .env file too.
With the db service of docker-compose.yml, export the database like this:
docker compose exec -T db pg_dump -U peristyle peristyle > peristyle.sql
The /data volume holds:
| Folder | Content |
|---|---|
archives/ | the copies of archived pages |
image-cache/ | the image cache, which can be rebuilt |
locales/, themes/, plugins/ | dropped languages, themes and extensions |
keys/ | the keys that encrypt the session cookies and the SMTP password saved in the administration |
Scheduled backup
Copy backup.sh next to docker-compose.yml. The script writes three dated files: the database (.dump), the /data volume without the image cache (.tar.gz) and a copy of .env. It then removes backups older than the given period, only if everything succeeded.
sh backup.sh /path/to/backups 14
Without arguments, it writes to ./backups and keeps 14 days. It only covers the db service of docker-compose.yml, not an existing PostgreSQL server.
On a Synology NAS, open Control Panel → Task Scheduler → Create → Scheduled Task → User-defined script:
- user:
root; - schedule: daily, at a quiet hour;
- task settings:
sh /volume1/docker/peristyle/backup.sh /volume1/backups/peristyle 14; - notification: send run details by e-mail on abnormal termination.
Start the task once with Run and check the files. If the script cannot find the container, the Container Manager project is not named after the folder: add COMPOSE_PROJECT_NAME=<project name> to .env.
On another Linux server, one crontab line is enough:
0 3 * * * sh /opt/peristyle/backup.sh /srv/backups/peristyle 14
.env, hence the database password: keep that copy private.Restoring
From the docker-compose.yml folder, with the .env of the backup and the same major version of PostgreSQL:
docker compose up -d db
docker compose stop web
docker compose exec -T db pg_restore -U peristyle -d peristyle --clean --if-exists --no-owner < peristyle-YYYY-MM-DD_HHMM.dump
docker run --rm -i --volumes-from "$(docker compose ps -aq web)" alpine:3.22 tar xzf - -C / < peristyle-YYYY-MM-DD_HHMM.tar.gz
docker compose up -d
On a new installation, run docker compose up -d first to create the containers and volumes.
Configuration
Settings come from environment variables (Peristyle__Name, nested with __). Most of them can also be changed in Administration → Settings or Administration → Email. The order of precedence is: value saved in the administration, then environment variable, then default.
The .env column gives the name to use in that file. A variable with no name in this column is not passed on by docker-compose.yml: add it under environment: of the web service, or use the administration.
Server settings
The words on restart mark those that only take effect at the next startup.
| Variable | .env | Default | Role |
|---|---|---|---|
Peristyle__AllowRegistration | ALLOW_REGISTRATION | true (false in docker-compose.yml without that line) | Allows sign-up. The very first account, administrator, is always allowed. |
Peristyle__AllowPrivateNetworkFeeds | — | false | Allows feeds on the local network (192.168.x.x…). On restart. |
Peristyle__RefreshIntervalMinutes | REFRESH_INTERVAL_MINUTES | 30 | Feed refresh interval. |
Peristyle__MaxConcurrentFetches | — | 4 | Simultaneous downloads. |
Peristyle__InitialArticlesPerSubscription | — | 50 | Recent articles marked unread when subscribing to a feed already known to the instance. |
Peristyle__ArticleRetentionDays | ARTICLE_RETENTION_DAYS | 90 | Article retention in days (0 = unlimited). Starred articles are always kept. |
Peristyle__DisableFailingFeedAfterDays | DISABLE_FAILING_FEED_AFTER_DAYS | 30 | Disables a feed failing for this many days (0 = never). Retry brings it back. |
Peristyle__SourceTimeoutSeconds | — | 60 | Maximum time to fetch and parse a feed. |
Peristyle__ProcessorTimeoutSeconds | — | 10 | Maximum time of one processing step on an article. Beyond that, the step is skipped. |
Peristyle__ArchiveQuotaMegabytes | — | 1024 | Archive space per user, in MB (0 = unlimited). |
Peristyle__MaxConcurrentArchives | — | 1 | Pages archived at the same time. On restart. |
Peristyle__LinkCheckIntervalDays | LINK_CHECK_INTERVAL_DAYS | 7 | Checks each bookmark’s address every N days (0 = never). A broken link is checked again the next day. On restart. |
Peristyle__LinkCheckBatchSize | — | 200 | Addresses checked at most per pass (one pass per hour). |
Peristyle__ImageProxy | IMAGE_PROXY | true | Image proxy: browsers no longer load images from the original sites. On restart. |
Peristyle__ImageCacheMegabytes | IMAGE_CACHE_MEGABYTES | 500 | Maximum size of the image cache, in MB. |
Peristyle__ImageCacheDays | — | 30 | How long an image stays in the cache, in days. |
Peristyle__MaxImageBytes | — | 5242880 | Maximum size of an image going through the proxy, in bytes (in MB in the administration). |
Peristyle__Plugins__RequireApproval | — | true | A dropped extension must be approved before it is loaded. On restart. |
Peristyle__DefaultLanguage | DEFAULT_LANGUAGE | fr | Language when the browser asks for none available, and for untranslated texts. On restart. |
Infrastructure
These variables stay in the environment, so a mistake in one of them can always be fixed.
| Variable | .env | Default | Role |
|---|---|---|---|
ConnectionStrings__peristyle | — | — | PostgreSQL connection string. docker-compose.yml builds it from POSTGRES_PASSWORD. |
Peristyle__TrustedProxies | TRUSTED_PROXIES | — | See Reverse proxy. |
Peristyle__DataPath | — | /data in the image | Keys, archives, image cache, dropped languages, themes and extensions. |
Peristyle__Plugins__SafeMode | PLUGINS_SAFE_MODE | false | Starts without any dropped extension. Bundled extensions stay active. |
TZ | TZ | Europe/Paris | Time zone of the instance, as a name such as America/Montreal. Dates are shown in this zone for every account: there is no per-account setting. |
DOTNET_GCHeapHardLimit | — | — | .NET heap cap, in hexadecimal (0x10000000 = 256 MB). |
OTEL_EXPORTER_OTLP_ENDPOINT | — | — | OpenTelemetry export, optional. Nothing is sent without this variable. |
E-mail (SMTP)
Without an SMTP server and a public address, Peristyle sends nothing: the administrator passes password and invitation links on by hand. With them, Peristyle sends:
- Forgot your password? links, only to a confirmed address;
- the confirmation of an address given at sign-up or changed in Settings → E-mail address;
- invitations;
- password links sent by an administrator, and a test e-mail.
Every e-mail is written in the recipient’s language: the language of their account, otherwise the language of the page they used, otherwise Peristyle__DefaultLanguage. Invitations use the language picked by the administrator, and the registration page opens in it.
Set the server in Administration → Email or with the variables below. Values saved in the administration take precedence; the password is encrypted there with the keys of /data/keys.
| Variable | .env | Default | Role |
|---|---|---|---|
Peristyle__PublicUrl | PUBLIC_URL | — | Address people use to open the instance, such as https://reader.example.org. Every link in an e-mail starts with it. Empty: no e-mail. |
Peristyle__Smtp__Host | SMTP_HOST | — | SMTP server. Empty: no e-mail. |
Peristyle__Smtp__Port | SMTP_PORT | 587 | Port (465 implicit SSL, 25 or 1025 local relay). |
Peristyle__Smtp__Security | SMTP_SECURITY | Auto | Auto (SSL on port 465, STARTTLS elsewhere when offered), StartTls, SslOnConnect or None. |
Peristyle__Smtp__UserName | SMTP_USERNAME | — | User name. Empty: no authentication. |
Peristyle__Smtp__Password | SMTP_PASSWORD | — | Password. |
Peristyle__Smtp__From | SMTP_FROM | — | Sender address, required. |
Peristyle__Smtp__FromName | SMTP_FROM_NAME | Peristyle | Sender name. |
Examples: Gmail smtp.gmail.com:587 and Outlook.com smtp-mail.outlook.com:587 with an app password; Synology MailPlus Server or another relay of the NAS with None on the local network.
API tokens
Each user creates their tokens in Settings → Applications, one per application, with an optional expiry (see the user guide). Peristyle only keeps the SHA-256 hash of a token: it is shown only when created and can be revoked at any time.
An application sends its token in the Authorization header. GET /api/me checks it: the answer gives the user name, or a 401 error when the token is invalid, revoked or expired.
curl -H "Authorization: Bearer ps_…" https://peristyle.example.com/api/me
A disabled account can no longer use its tokens.
Extensions
Article processing goes through extensions. There are three kinds:
- Shipped with Peristyle: podcasts, automatic full article, lead image of the full article, YouTube videos and slideshow. Active by default, they can be disabled.
- Per feed: such an extension only applies to the feeds where a subscriber turned it on, in Feed settings… → Content. The choice is shared by the feed’s subscribers and reprocesses its 20 most recent articles.
- Dropped: a folder
/data/plugins/<id>/holdingmanifest.json, the assemblies and, if needed,wwwroot/for scripts and styles. It stays Pending until approved in Administration → Extensions.
A modified extension goes back to pending, because Peristyle compares the SHA-256 hash of its folder. Decisions apply at restart.
If an extension prevents startup, start Peristyle with PLUGINS_SAFE_MODE=true: no dropped extension is loaded, and you can disable it.
Languages
The interface is available in French and English. Everyone picks a language in their Settings or on the sign-in page; otherwise Peristyle follows the browser.
To add a language or change a few texts without rebuilding the image, drop a file into /data/locales/, named after the language code (de.json, pt-BR.json, or fr.json to override French), then restart the container.
{
"Language": { "Name": "Deutsch" },
"Nav": { "AllArticles": "Alle Artikel", "Starred": "Favoriten" }
}
- Only the keys present are replaced; any missing text is shown in the default language.
- Keys can be nested, as above, or flat (
"Nav.Starred": "…"). {0},{1}… are parameters; keys ending in.Oneand.Otherare the singular and plural forms.- E-mails are translated the same way (
Email.*keys).
data volume of docker-compose.yml is a named volume. To drop files easily, replace it with a server folder, for example ./data:/data.Themes
Everyone picks a theme in their Settings: Same as system, Light, Dark or an installed theme.
An installed theme is a folder of /data/themes/, whose name (lowercase letters, digits, dashes) is its identifier. It holds two required files and, if needed, .woff2 fonts and images.
/data/themes/sepia/
├── manifest.json { "name": "Sépia", "author": "…", "version": "1.0.0", "scheme": "light" }
├── theme.css redefines the CSS variables it wants
└── fonts/… referenced relatively: url(fonts/literata.woff2)
scheme(lightordark) gives the base palette; the theme only redefines what it wants to change.- The available variables are listed at the top of
src/Peristyle.Web/wwwroot/app.css, andsamples/themes/sepiais a full example. - No JavaScript and no external resource: a
theme.csscontaining@import, an absoluteurl()or aurl()going up (../) is ignored, with a warning in the logs. - Themes are read at startup: restart the container after adding one or changing a manifest.
Administration
The Administration entry of the sidebar appears for administrators. The first account is one, and can appoint others. Each topic has its own tab.
Dashboard
An overview of the instance, with its version: Accounts, Feeds (including failing, disabled or overdue ones), Content and Server (database size and status, running queues). Refresh computes the figures again.
Accounts
The list of accounts, with their address, last sign-in and data volume. On someone else’s account:
- Make administrator or Remove admin rights;
- Disable or Enable: a disabled account can no longer sign in or use its tokens;
- New password link: a link to send to the person, valid once, within 24 hours;
- Send the link by e-mail, when the account’s address is confirmed and e-mail is configured;
- Delete: type the account name, then Delete permanently.
Invitations
An invitation link lets someone create an account even when registration is closed. It works only once. The Registration card shows whether it is Open or Closed.
In New invitation, say who it is for (optional) and pick a validity of 1, 7 or 30 days, then Create link. Copy the link right away: it will not be shown again. When e-mail is configured, you can also enter an address and choose the E-mail language.
The list gives the state of each invitation: Pending, Used or Expired. Revoke cancels a pending invitation.
Settings
The server settings, by topic: Registration, Feeds, Retention, Archives, Bookmarks, Images, Language and Extensions. Each line says where its value comes from.
Save keeps the value in the database, where it overrides the environment variable. Reset goes back to that variable. When a setting marked on restart has changed, a banner offers Restart Peristyle: the application stops cleanly and Docker starts it again.
Status says whether sending is On or Off: it needs at least the public address, the server and the sender address. Outgoing server (SMTP) holds the settings of the E-mail (SMTP) section, with no restart. Send a test e-mail checks the configuration; any server error is shown on the page.
Extensions
Each extension shows its version, its hash, the capabilities it asks for (network, readable, script, frame), its hooks and, once loaded, its calls, errors and average time.
- Bundled extension: Disable or Enable.
- Dropped extension: Approve, Disable or Reject. An invalid or incompatible extension cannot be approved; its problems are listed.
The statuses are Pending, Approved, Rejected, Disabled and Failed. A decision applies at restart: Restart Peristyle appears when needed.
Storage
The space used, calculated when the page opens, which can take a few seconds:
- Database, by category;
- Files: Archived pages and Image cache;
- By user: a feed followed by several people is shared between them pro rata.