Your own sync server
Polypodium works 100% offline — the server is only needed if you want to sync your collection across multiple devices. This guide shows how to get the server running with Docker in a few minutes, no compiling required.
What you'll need
- A machine that stays on whenever you want to sync: a VPS, a mini PC, a NAS, or even a computer on your local network.
- Docker with the Docker Compose plugin (included in current installations).
- Optional, but recommended for access over the internet: a domain (or subdomain) pointing at the machine, to enable HTTPS with Let's Encrypt.
No need to clone a repository or install Dart: the server ships as a ready-made
Docker image at ghcr.io/bruno1pb13/polypodium_server.
1 Create the configuration files
Create a folder (for example polypodium-server) and, inside it, a
docker-compose.yml file with the contents below. It starts two
containers: the PostgreSQL database and the Polypodium server.
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: polypodium
POSTGRES_USER: polypodium
POSTGRES_PASSWORD: ${DB_PASSWORD:?DB_PASSWORD is required}
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U polypodium -d polypodium"]
interval: 5s
timeout: 5s
retries: 10
server:
image: ghcr.io/bruno1pb13/polypodium_server:latest
restart: unless-stopped
ports:
- "8080:8080"
environment:
DATABASE_URL: postgresql://polypodium:${DB_PASSWORD}@db/polypodium
JWT_SECRET: ${JWT_SECRET:?JWT_SECRET is required}
APP_ENV: production
DB_SSL: "false"
# HTTPS is handled by the reverse proxy (step 3).
BEHIND_PROXY: "true"
REGISTRATION_TOKEN: ${REGISTRATION_TOKEN:-}
PHOTOS_DIR: /photos
volumes:
- photos_data:/photos
depends_on:
db:
condition: service_healthy
volumes:
db_data:
photos_data:
In the same folder, create a .env file with your server's secrets:
# Database password (used only internally, between the containers)
DB_PASSWORD=replace-with-a-strong-password
# Key that signs the login tokens. Minimum of 32 characters.
# Generate one with: openssl rand -base64 48
JWT_SECRET=replace-with-a-long-random-key
# Optional, recommended if the server is exposed to the internet:
# protects the creation of the first account (see step 4).
REGISTRATION_TOKEN=
JWT_SECRET is missing or shorter than 32 characters. Keep the
.env file somewhere safe — whoever has these values has access to the server.
2 Start the server
Still inside the folder you created, run:
docker compose up -d
On the first run Docker downloads the images and the server creates the database tables automatically. To check that everything is up:
curl http://localhost:8080/api/v1/health
If the response comes back with status 200, the server is working. To follow the
logs at any time: docker compose logs -f server.
http://192.168.0.10:8080), you can skip straight to step 4.
The HTTPS setup in step 3 is essential when the server is reachable from the internet.
3 HTTPS with a reverse proxy (Let's Encrypt)
The configuration above (BEHIND_PROXY: "true") expects a reverse proxy
in front of the server to handle HTTPS. The simplest option is
Caddy, which obtains and renews
Let's Encrypt certificates on its own. With your domain already pointing at the
machine, install Caddy and use a two-line Caddyfile:
plants.yourdomain.com {
reverse_proxy 127.0.0.1:8080
}
Done: the app can now reach https://plants.yourdomain.com. Alternatives
that work just as well, with the same BEHIND_PROXY=true:
- Nginx Proxy Manager — a web UI for managing proxies and Let's Encrypt certificates;
- Nginx or Traefik, if you already run one of them on your server;
- A tunnel such as Cloudflare Tunnel, which also terminates TLS for you.
In this setup, keep port 8080 closed to the internet in your firewall — only the proxy (ports 80/443) needs to be exposed.
Alternative: the server terminates HTTPS itself
If you'd rather not use a proxy, the server can serve HTTPS directly. In
docker-compose.yml, replace BEHIND_PROXY: "true" with:
BEHIND_PROXY: "false"
SSL_CERT_PATH: /certs/fullchain.pem
SSL_KEY_PATH: /certs/privkey.pem
and mount the certificates folder into the container (under volumes: of
the server service):
- /path/to/your/certs:/certs:ro
In this case, renewing the certificates (e.g. via certbot) is up to you.
In production the server requires one of the two options — reverse proxy or
certificates — and refuses to start serving plain HTTP on the internet.
4 Create the first account (administrator)
The first account created on a fresh server automatically becomes the administrator account. After that, public registration closes: new accounts can only be created by the administrator, inside the app.
Without REGISTRATION_TOKEN (local network)
- In the app, open Settings → Server (workspaces) and add a remote workspace with your server's URL.
- The app detects that the server has no accounts yet and offers to create the first one — enter an e-mail and password (minimum of 8 characters) and that's it: you're the admin.
With REGISTRATION_TOKEN (recommended on the internet)
If you set REGISTRATION_TOKEN in .env, the first account must
present that token — so nobody "steals" the admin seat by getting to your freshly
installed server first. Create the account with a direct API call:
curl -X POST https://plants.yourdomain.com/api/v1/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "you@example.com",
"password": "your-8+-character-password",
"registrationToken": "the-value-of-your-REGISTRATION_TOKEN"
}'
After that, simply log in normally through the app with the e-mail and password you created. Further accounts (family, friends) are created from the server administration screen, inside the app itself.
5 Backups
Your data lives in two places: the PostgreSQL database (plants, species, journal…) and the photos volume. Back up both periodically.
Database (pg_dump)
docker compose exec db pg_dump -U polypodium -d polypodium \
> backup-polypodium-$(date +%F).sql
To restore onto a freshly created server (with the database still empty):
docker compose exec -T db psql -U polypodium -d polypodium \
< backup-polypodium-2026-07-10.sql
Photos
docker compose cp server:/photos ./photos-backup-$(date +%F)
Automate it with a weekly cron job and keep the copies on another machine
or a storage service — a backup that lives on the same disk as the server doesn't
protect you from that disk failing.
Updating the server
To update to the latest image version:
docker compose pull
docker compose up -d
Database migrations run automatically at startup — there is no manual step.
Reference: environment variables
| Variable | What it's for |
|---|---|
DB_PASSWORD | PostgreSQL password (required). |
JWT_SECRET | Signs the login tokens. Required, minimum of 32 characters, unique per server. |
REGISTRATION_TOKEN | Optional. When set, creating the first account requires this token (step 4). |
BEHIND_PROXY | true when HTTPS terminates at a reverse proxy; otherwise provide SSL_CERT_PATH/SSL_KEY_PATH. |
SSL_CERT_PATH / SSL_KEY_PATH | Certificate and key for the server to serve HTTPS directly. |
ALLOWED_ORIGINS | CORS: * or a comma-separated list of origins. Default: *. |
AUTH_RATE_LIMIT_MAX / AUTH_RATE_LIMIT_WINDOW | Brute-force protection on login. Default: 20 requests / 300 s per IP. |
MAX_JSON_BODY_BYTES / MAX_PHOTO_BYTES | Maximum request and photo sizes. Default: 1 MB / 15 MB. |
Questions or problems? Open an issue on the server repository or write to bruno1pb13@gmail.com.