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

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=
Important: in production, the server refuses to start if 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.

Local network only? If the server will only be reached from inside your home (e.g. 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:

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)

  1. In the app, open Settings → Server (workspaces) and add a remote workspace with your server's URL.
  2. 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

VariableWhat it's for
DB_PASSWORDPostgreSQL password (required).
JWT_SECRETSigns the login tokens. Required, minimum of 32 characters, unique per server.
REGISTRATION_TOKENOptional. When set, creating the first account requires this token (step 4).
BEHIND_PROXYtrue when HTTPS terminates at a reverse proxy; otherwise provide SSL_CERT_PATH/SSL_KEY_PATH.
SSL_CERT_PATH / SSL_KEY_PATHCertificate and key for the server to serve HTTPS directly.
ALLOWED_ORIGINSCORS: * or a comma-separated list of origins. Default: *.
AUTH_RATE_LIMIT_MAX / AUTH_RATE_LIMIT_WINDOWBrute-force protection on login. Default: 20 requests / 300 s per IP.
MAX_JSON_BODY_BYTES / MAX_PHOTO_BYTESMaximum 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.