Skip to content

Install with docker run

This guide is for a quick trial. Select a Lunar Paper version, then start it with official PostgreSQL using docker run. It starts four containers: lunarpaper-postgres, lunarpaper-redis, lunarpaper-seaweedfs, and lunarpaper, which serves Lunar Paper on host port 3000 by default. You can choose another host port.

  • Install Docker and open a terminal (PowerShell on Windows).
  • Choose an unused host port. The example uses 3000; you can change it.
  • Choose a published platform release. These examples target 0.8.1 in lunarpaper/lunarpaper. Its public image must be published before it can be pulled.
  • Linux and macOS need OpenSSL to generate secrets.

Create a working folder and run the remaining commands from it:

Terminal window
mkdir lunarpaper
cd lunarpaper

Set HOST_PORT and include the same port in APP_URL in step 1. For example, use HOST_PORT=3100 with APP_URL=http://localhost:3100. The commands map this host port to the application’s fixed container port 3000.

HTTP_BIND_ADDRESS defaults to 127.0.0.1 for access from this computer only. For LAN access, set it to an IP actually assigned to the server and use that address in APP_URL. Changing APP_URL alone does not change the port or interface.

Running multiple instances

For a second fresh instance, use the Compose multiple-instance instructions. These docker run commands use fixed container, network and volume names. COMPOSE_PROJECT_NAME does not affect docker run; changing only the folder or host port does not separate its storage.

The volumes also use the main Compose guide’s default lunarpaper_ prefix to support the migration instructions below. Do not run this fresh-install path against an existing installation’s volumes. Retained PostgreSQL volumes keep their original passwords even after containers are removed. Restore the saved settings for an existing installation; generate new secrets only for empty storage.

  1. Set your configuration. Edit the address and administrator details, then paste this into your terminal.

    Terminal window
    export LUNAR_PAPER_VERSION=0.8.1
    export LUNAR_PAPER_IMAGE="ghcr.io/lunarpaper/platform"
    export POSTGRES_IMAGE=postgres:17-alpine
    export PGUSER=postgres
    export APP_PLATFORM_USER=app_platform
    export APP_INTAKE_USER=app_intake
    export APP_URL=http://localhost:3000
    export HOST_PORT=3000
    export HTTP_BIND_ADDRESS=127.0.0.1
    export COOKIE_NAMESPACE=
    export SYSTEM_ADMIN_USERNAME=you@example.com
    export SYSTEM_ADMIN_PASSWORD='replace-with-your-password'

    APP_URL is the address you open in the browser. An address with localhost only works on this computer. The password needs 12 or more characters, with upper case, lower case, a number and a symbol, and no spaces or quotes.

    PGUSER defaults to postgres and names the owner used for setup and upgrades. Choose it before creating the PostgreSQL volume. Use lowercase letters, numbers and underscores; avoid app_*, pg_*, svc_datalayer and reporting. The application usernames also have defaults that you can override before setup. See Database usernames for naming rules. Every database password is independently generated.

  2. Generate new secrets in the same terminal. For an existing installation, restore its saved settings instead; new passwords do not update existing databases.

    Terminal window
    for name in SETUP_KEY PGPASSWORD APP_PLATFORM_PASSWORD APP_INTAKE_PASSWORD REDIS_PASSWORD \
    SESSION_SECRET ENCRYPTION_KEY INTERNAL_API_TOKEN AI_ENCRYPTION_KEY MFA_ENCRYPTION_KEY \
    SSO_ENCRYPTION_KEY DATA_SOURCE_ENCRYPTION_KEY WORKFLOW_CREDENTIAL_ENCRYPTION_KEY \
    PUBLISHER_CREDENTIAL_ENCRYPTION_KEY EMAIL_ENCRYPTION_KEY ADDRESS_PROVIDER_ENCRYPTION_KEY \
    EMAIL_TRACKING_SECRET PDF_EXPORT_SECRET; do
    export "$name=$(openssl rand -hex 32)"
    done
    export FORM_FILE_ENCRYPTION_KEYS="{\"v1\":\"$(openssl rand -hex 32)\"}"
  3. Save your settings. Copy this command’s output into a password manager. It contains all your secrets.

    Terminal window
    for name in LUNAR_PAPER_VERSION LUNAR_PAPER_IMAGE POSTGRES_IMAGE PGUSER APP_PLATFORM_USER APP_INTAKE_USER APP_URL HOST_PORT HTTP_BIND_ADDRESS COOKIE_NAMESPACE SYSTEM_ADMIN_USERNAME SYSTEM_ADMIN_PASSWORD \
    SETUP_KEY PGPASSWORD APP_PLATFORM_PASSWORD APP_INTAKE_PASSWORD REDIS_PASSWORD \
    SESSION_SECRET ENCRYPTION_KEY INTERNAL_API_TOKEN AI_ENCRYPTION_KEY MFA_ENCRYPTION_KEY \
    SSO_ENCRYPTION_KEY DATA_SOURCE_ENCRYPTION_KEY WORKFLOW_CREDENTIAL_ENCRYPTION_KEY \
    PUBLISHER_CREDENTIAL_ENCRYPTION_KEY EMAIL_ENCRYPTION_KEY ADDRESS_PROVIDER_ENCRYPTION_KEY \
    EMAIL_TRACKING_SECRET PDF_EXPORT_SECRET FORM_FILE_ENCRYPTION_KEYS; do
    printf "export %s='%s'\n" "$name" "$(printenv "$name")"
    done

    Restore this block in each new terminal and before upgrades. It preserves your image versions, address, port, usernames and secrets.

  4. Create the network.

    Terminal window
    docker network create lunarpaper
  5. Start the database.

    Terminal window
    docker run -d --name lunarpaper-postgres --network lunarpaper --network-alias postgres \
    --restart unless-stopped \
    -e POSTGRES_DB=platform -e POSTGRES_USER="$PGUSER" -e POSTGRES_PASSWORD="$PGPASSWORD" \
    --health-cmd='pg_isready -U $POSTGRES_USER -d platform' --health-interval=5s --health-retries=12 \
    -v lunarpaper_postgres-data:/var/lib/postgresql/data \
    "$POSTGRES_IMAGE" postgres -c max_connections=200
  6. Start Redis and file storage.

    Create filer.toml with notepad filer.toml or nano filer.toml and paste:

    Create filer.toml
    [leveldb2]
    enabled = true
    dir = "/data/filerldb2"

    Start Redis and SeaweedFS after saving the file:

    Terminal window
    docker run -d --name lunarpaper-redis --network lunarpaper --network-alias redis \
    --restart unless-stopped -v lunarpaper_redis-data:/data -e REDIS_PASSWORD \
    redis:7-alpine redis-server --appendonly yes --requirepass "$REDIS_PASSWORD"
    docker run -d --name lunarpaper-seaweedfs --network lunarpaper --network-alias seaweedfs \
    --restart unless-stopped -w /data -v lunarpaper_seaweedfs-data:/data \
    -v "$PWD/filer.toml:/etc/seaweedfs/filer.toml:ro" \
    chrislusf/seaweedfs:4.47 server -ip=seaweedfs -ip.bind=0.0.0.0 -dir=/data \
    -master.volumeSizeLimitMB=1024 -volume.max=0 -filer
  7. Start Lunar Paper. The first line waits until the database is ready.

    Terminal window
    until [ "$(docker inspect -f '{{.State.Health.Status}}' lunarpaper-postgres)" = healthy ]; do sleep 2; done
    docker run -d --name lunarpaper --network lunarpaper --restart unless-stopped \
    -p "${HTTP_BIND_ADDRESS}:${HOST_PORT}:3000" -v lunarpaper_uploads-data:/app/uploads \
    -e NODE_ENV=production -e PORT=3000 -e PROCESS_ROLE=all -e CLUSTER_WORKERS=1 -e TRUST_PROXY_HOPS=0 \
    -e APP_URL -e API_URL="$APP_URL" -e ALLOWED_ORIGINS="$APP_URL" -e PUBLISHER_CALLBACK_ORIGINS="$APP_URL" \
    -e LUNAR_PAPER_IMAGE="${LUNAR_PAPER_IMAGE}:${LUNAR_PAPER_VERSION}" \
    -e SETUP_KEY -e SYSTEM_ADMIN_USERNAME -e SYSTEM_ADMIN_PASSWORD \
    -e APP_PLATFORM_PASSWORD -e APP_INTAKE_PASSWORD \
    -e PUBLISHER_PRIVATE_TARGETS -e COOKIE_NAMESPACE \
    -e SESSION_SECRET -e ENCRYPTION_KEY -e INTERNAL_API_TOKEN -e AI_ENCRYPTION_KEY -e MFA_ENCRYPTION_KEY \
    -e SSO_ENCRYPTION_KEY -e DATA_SOURCE_ENCRYPTION_KEY -e WORKFLOW_CREDENTIAL_ENCRYPTION_KEY \
    -e PUBLISHER_CREDENTIAL_ENCRYPTION_KEY -e EMAIL_ENCRYPTION_KEY -e ADDRESS_PROVIDER_ENCRYPTION_KEY \
    -e EMAIL_TRACKING_SECRET -e PDF_EXPORT_SECRET \
    -e FORM_FILE_ENCRYPTION_KEYS -e FORM_FILE_ENCRYPTION_ACTIVE_KEY_ID=v1 \
    -e PGHOST=postgres -e PGPORT=5432 -e PGDATABASE=platform -e PGUSER -e PGPASSWORD -e APP_PLATFORM_USER -e APP_INTAKE_USER \
    -e DATABASE_URL="postgresql://${APP_PLATFORM_USER}:$APP_PLATFORM_PASSWORD@postgres:5432/platform" \
    -e INTAKE_DATABASE_URL="postgresql://${APP_INTAKE_USER}:$APP_INTAKE_PASSWORD@postgres:5432/intake" \
    -e MIGRATION_PLATFORM_DATABASE_URL="postgresql://${PGUSER}:$PGPASSWORD@postgres:5432/platform" \
    -e MIGRATION_INTAKE_DATABASE_URL="postgresql://${PGUSER}:$PGPASSWORD@postgres:5432/intake" \
    -e APP_DB_ROLE=true -e DB_POOL_MAX=40 \
    -e SESSION_STORE=redis -e REDIS_REQUIRED=true -e REDIS_HOST=redis -e REDIS_PORT=6379 -e REDIS_PASSWORD \
    -e SEAWEEDFS_MASTER_URL=http://seaweedfs:9333 -e SEAWEEDFS_FILER_URL=http://seaweedfs:8888 \
    -e SEAWEEDFS_VOLUME_URL=http://seaweedfs:8080 \
    "${LUNAR_PAPER_IMAGE}:${LUNAR_PAPER_VERSION}"
  8. Open your APP_URL and initialize. Unlock setup with SETUP_KEY, review the empty databases and explicitly start initialization. The platform imports its bundled init files and verifies Platform, Intake and release activation. Launch the application after completion, then sign in. See First-time setup.

If you see a maintenance page instead, follow Complete the database upgrade.

All four containers should show Up, and lunarpaper-postgres and lunarpaper should also show (healthy):

Terminal window
docker ps --filter name=lunarpaper

Watch the log; press Ctrl+C to stop:

Terminal window
docker logs -f lunarpaper

Lunar Paper is ready when this returns HTTP 200. If you chose a different host port, replace 3000 in these URLs with that port:

Terminal window
curl -fsS http://localhost:3000/api/health/ready
  1. Paste your saved block, then set APP_URL to this computer’s network address:

    Terminal window
    export HTTP_BIND_ADDRESS=192.168.1.20
    export APP_URL="http://${HTTP_BIND_ADDRESS}:${HOST_PORT}"

    Replace 192.168.1.20 with an IP actually assigned to your server.

  2. Remove the lunarpaper container, then run step 7 again. Your data is not affected.

    Terminal window
    docker rm -f lunarpaper
  3. Run step 3 again and save the new block, because APP_URL has changed.

For firewall settings and the limits of HTTP, see HTTP on a local network.

The containers start again whenever Docker starts, unless you stop them. To stop Lunar Paper:

Terminal window
docker stop lunarpaper lunarpaper-seaweedfs lunarpaper-redis lunarpaper-postgres

To start it again:

Terminal window
docker start lunarpaper-postgres lunarpaper-redis lunarpaper-seaweedfs lunarpaper
  1. Take a backup.

  2. Read the release notes and paste your saved settings block. Change LUNAR_PAPER_VERSION to the new published version. Keep LUNAR_PAPER_IMAGE as the repository name without a tag. Preserve all secrets and your chosen host port. Keep PostgreSQL on its existing image.

  3. Pull the new platform image, then remove only the application container. Your databases and data volumes stay in place.

    Terminal window
    docker pull "${LUNAR_PAPER_IMAGE}:${LUNAR_PAPER_VERSION}"
    Terminal window
    docker rm -f lunarpaper
  4. Run step 7 again to create the platform container with its new image.

  5. If a maintenance page appears, complete the database upgrade.

  6. Run step 3 of the installation again to save your updated settings block.

Docker Compose uses the same volumes as these commands, so your data moves with you.

  1. Create a lunarpaper folder and create compose.yaml from step 4 of the Docker Compose guide.

  2. Open a terminal in that folder, paste your saved block, and then run this command to write it to .env beside compose.yaml. Use this only when .env does not already exist; never overwrite an existing environment file:

    Terminal window
    if [ -e .env ]; then echo 'Stop: .env already exists'; else
    for name in LUNAR_PAPER_VERSION LUNAR_PAPER_IMAGE POSTGRES_IMAGE PGUSER APP_PLATFORM_USER APP_INTAKE_USER APP_URL HOST_PORT HTTP_BIND_ADDRESS COOKIE_NAMESPACE SYSTEM_ADMIN_USERNAME SYSTEM_ADMIN_PASSWORD \
    SETUP_KEY PGPASSWORD APP_PLATFORM_PASSWORD APP_INTAKE_PASSWORD REDIS_PASSWORD \
    SESSION_SECRET ENCRYPTION_KEY INTERNAL_API_TOKEN AI_ENCRYPTION_KEY MFA_ENCRYPTION_KEY \
    SSO_ENCRYPTION_KEY DATA_SOURCE_ENCRYPTION_KEY WORKFLOW_CREDENTIAL_ENCRYPTION_KEY \
    PUBLISHER_CREDENTIAL_ENCRYPTION_KEY EMAIL_ENCRYPTION_KEY ADDRESS_PROVIDER_ENCRYPTION_KEY \
    EMAIL_TRACKING_SECRET PDF_EXPORT_SECRET FORM_FILE_ENCRYPTION_KEYS; do
    printf '%s=%s\n' "$name" "$(printenv "$name")"
    done > .env
    chmod 600 .env
    fi

    The saved settings include HTTP_BIND_ADDRESS, HOST_PORT and COOKIE_NAMESPACE. Preserve them when moving to Compose.

  3. Remove the containers and the network. The volumes, which hold your data, are kept.

    Terminal window
    docker rm -f lunarpaper lunarpaper-seaweedfs lunarpaper-redis lunarpaper-postgres
    docker network rm lunarpaper
  4. Start Lunar Paper with Docker Compose:

    Terminal window
    docker compose up -d --wait

    Keep the project name lunarpaper so Compose uses the existing volumes. Warnings that the volumes “already exist” are expected.

To remove the containers and the network:

Terminal window
docker rm -f lunarpaper lunarpaper-seaweedfs lunarpaper-redis lunarpaper-postgres
docker network rm lunarpaper

Your data stays in the volumes until you delete them.

  • Lunar Paper keeps restarting. Run docker logs lunarpaper to see why. The usual cause is a password that breaks the rules, or a setting that was empty. Paste your saved block, run docker rm -f lunarpaper, and run step 7 again.
  • The settings are gone in a new terminal. Settings only last for the terminal you set them in. Paste your saved block again.
  • Port 3000 is in use. Set HOST_PORT to an available port, include it in APP_URL, and use those settings when creating the application container.
  • Database login fails with 28P01. Restore the settings that belong to the retained PostgreSQL volume. New environment values do not replace stored passwords. For a fresh parallel installation, use Compose with a new project name.
  • Sign-in keeps returning to the sign-in page. Make sure the address in your browser is exactly the same as APP_URL. 127.0.0.1 and localhost count as different addresses.
  • Step 7 never finishes. Press Ctrl+C and run docker logs lunarpaper-postgres to see why the database did not start.
  • An image download fails. Check that the release is published and that LUNAR_PAPER_IMAGE is the repository without a tag and LUNAR_PAPER_VERSION is the published version selected in step 1. Official PostgreSQL has an independent version; it never uses a Lunar Paper release tag.