Skip to content

Set up a publishing instance

A publishing instance is a small, separate server that shows the forms, pages, documents and slideshows you publish to people who do not sign in to Lunar Paper. Your main installation can stay private: the publishing instance is the only part people outside your organisation reach.

You need a working Lunar Paper installation first, from any of the install guides. The publishing instance can run on the same server or on another one.

The publishing instance does not run PostgreSQL. Your main Lunar Paper installation owns the Platform and Intake databases and their protected setup and upgrade flow. Use the publisher image for your selected platform release; there is no additional Lunar Paper database image to install.

  • When you publish something, Lunar Paper sends it to the publishing instance’s address.
  • Form submissions and uploaded files go from the publishing instance back to Lunar Paper at MAIN_APP_URL, which must be the same as the main installation’s APP_URL. The publishing instance stores no submissions.
  • People open published content at the publishing instance’s address.

The address you give the publishing instance decides what else you need:

Address Example What you need
On your own network http://192.168.1.20:8081 List it in PUBLISHER_PRIVATE_TARGETS on the main installation
On the internet https://forms.example.com A domain and a reverse proxy with HTTPS in front of port 8081
  • Install Lunar Paper and sign in as an administrator.
  • Select a published publisher image matching your platform release. The examples target 0.8.1; its image must be published before it can be pulled.
  • The main installation’s APP_URL must be an address the publishing instance can reach, such as http://192.168.1.20:3000. An address with localhost does not work, because inside the publishing instance it means the instance itself.
  • Choose an available publisher host port. The default is 8081; set PUBLISHER_HOST_PORT to use another port and register that port in its address.

Skip this section if the publishing instance has a public https:// address.

Lunar Paper refuses to send content to an address on a private network unless you list it. Add the address, as host:port, to the main installation’s .env file:

PUBLISHER_PRIVATE_TARGETS=192.168.1.20:8081

Then apply it in the main installation’s folder:

Terminal window
docker compose up -d --wait

To list more than one publishing instance, separate them with commas. For a main installation using HTTPS, use the HTTPS startup command. The publishing instance’s own commands below use its separate folder and Compose file.

If the publishing instance is already running, for example because you set it up for another Lunar Paper installation, skip to Connect an instance that is already running.

  1. In Lunar Paper, open Administration → Publisher Services → Publisher Inventory.

  2. Under Create New Publisher, enter a name and the publishing instance’s address, such as http://192.168.1.20:8081. Leave the API key empty so Lunar Paper generates one, then select Create Publisher.

  3. Lunar Paper shows the settings for the publishing instance. Select Copy settings and keep them somewhere safe until the next section.

    ASSET_ENCRYPTION_KEY and EMAIL_TRACKING_SECRET are shown only this once. You can show the other settings again at any time with Connection settings.

Running multiple publishing instances

Use a separate folder and register each new publisher to obtain its own connection settings. In the new publisher’s .env, set:

COMPOSE_PROJECT_NAME=lunarpaper-publisher-test
PUBLISHER_HOST_PORT=8082
PUBLISHER_DATA=publisher-data

Choose an unused project name and port. Named storage becomes lunarpaper-publisher-test_publisher-data. Use separate paths if selecting host folders instead. Keep existing secrets with existing content; do not overwrite another publisher’s .env. Register the new port in Publisher Inventory and, for private addresses, in the main app’s PUBLISHER_PRIVATE_TARGETS.

  1. Create the publisher folder on its server.

    Terminal window
    mkdir lunarpaper-publisher
    cd lunarpaper-publisher
  2. Create .env with nano .env or notepad .env, and paste the settings you copied from Publisher Inventory. Add LUNAR_PAPER_VERSION for the matching platform release. Keep existing files and keys when reusing a publisher.

    View the .env reference

    This shows the file structure. Use the full settings copied from Lunar Paper; the shortened secrets below cannot be used. Publisher credentials come from registration, not the main application’s .env generator.

    LUNAR_PAPER_VERSION=0.8.1
    # Optional publisher image repository override, without a tag.
    # PUBLISHER_IMAGE=ghcr.io/lunarpaper/publisher
    PUBLISHER_HOST_PORT=8081
    PUBLISHER_BIND_ADDRESS=0.0.0.0
    PUBLISHER_DATA=publisher-data
    MAIN_APP_URL=http://192.168.1.20:3000
    PUBLISHER_INSTANCE_ID=0b6e3c2a-5d41-4f7e-9a8c-2f1d6e4b7a90
    API_KEY=4f1c9a0e…b27d
    PUBLISHER_BROKER_TOKEN=9d2e61b4…0c5a
    ASSET_ENCRYPTION_KEY=c83a7f15…e941
    EMAIL_TRACKING_SECRET=1b7d4e09…6fa2

    PUBLISHER_BIND_ADDRESS=0.0.0.0 listens on all server interfaces. Use an actual server interface IP to restrict access, or 127.0.0.1 if a reverse proxy on the same host is the only caller. The main app must be able to reach the registered publisher address. Changing MAIN_APP_URL does not change the publisher’s port.

    Keep a copy of this file with your backups. ASSET_ENCRYPTION_KEY protects the data that published forms show from datasets; if it is lost or changed, those forms have to be published again. If you use email tracking, set EMAIL_TRACKING_SECRET to the same value as the main installation’s EMAIL_TRACKING_SECRET.

  3. Generate compose.yaml. Paste this into your terminal. Existing files are kept unchanged.

    Terminal window
    & {
    $ErrorActionPreference = 'Stop'
    $files = [ordered]@{
    'compose.yaml' = @'
    # A Lunar Paper publishing instance. It shows published forms, pages,
    # documents and slideshows to people outside Lunar Paper.
    # Keep this file and your .env file together in one folder, then run:
    # docker compose up -d --wait
    # To upgrade, change LUNAR_PAPER_VERSION in .env to match your main
    # installation and run the same command.
    name: lunarpaper-publisher
    services:
    publisher:
    image: ${PUBLISHER_IMAGE:-ghcr.io/lunarpaper/publisher}:${LUNAR_PAPER_VERSION:?Set LUNAR_PAPER_VERSION in .env}
    restart: unless-stopped
    ports:
    - "${PUBLISHER_BIND_ADDRESS:-0.0.0.0}:${PUBLISHER_HOST_PORT:-8081}:8080"
    environment:
    NODE_ENV: production
    # The main Lunar Paper address. It must be the same as its APP_URL.
    MAIN_APP_URL: ${MAIN_APP_URL:?Set MAIN_APP_URL in .env}
    PUBLISHER_INSTANCE_ID: ${PUBLISHER_INSTANCE_ID:?Set PUBLISHER_INSTANCE_ID in .env}
    API_KEY: ${API_KEY:?Set API_KEY in .env}
    PUBLISHER_BROKER_TOKEN: ${PUBLISHER_BROKER_TOKEN:?Set PUBLISHER_BROKER_TOKEN in .env}
    ASSET_ENCRYPTION_KEY: ${ASSET_ENCRYPTION_KEY:?Set ASSET_ENCRYPTION_KEY in .env}
    EMAIL_TRACKING_SECRET: ${EMAIL_TRACKING_SECRET:?Set EMAIL_TRACKING_SECRET in .env}
    # 0 when people connect directly, 1 behind a reverse proxy.
    PUBLISHER_TRUST_PROXY_HOPS: ${PUBLISHER_TRUST_PROXY_HOPS:-0}
    volumes:
    - "${PUBLISHER_DATA:-publisher-data}:/app/data"
    healthcheck:
    test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:8080/health"]
    interval: 30s
    timeout: 10s
    retries: 3
    start_period: 40s
    volumes:
    publisher-data:
    '@
    }
    foreach ($file in $files.GetEnumerator()) {
    $destination = Join-Path . $file.Key
    if (Test-Path -LiteralPath $destination) {
    Write-Host "Kept existing $destination"
    continue
    }
    $stream = [System.IO.File]::Open($ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($destination), [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write)
    try {
    $bytes = [System.Text.Encoding]::UTF8.GetBytes($file.Value + [Environment]::NewLine)
    $stream.Write($bytes, 0, $bytes.Length)
    } finally { $stream.Dispose() }
    Write-Host "Created $destination"
    }
    }
    Optionally create compose.yaml manually

    Create compose.yaml beside .env and paste this configuration. Keep the ${VARIABLE} expressions; Compose fills them from .env.

    compose.yaml
    # A Lunar Paper publishing instance. It shows published forms, pages,
    # documents and slideshows to people outside Lunar Paper.
    # Keep this file and your .env file together in one folder, then run:
    # docker compose up -d --wait
    # To upgrade, change LUNAR_PAPER_VERSION in .env to match your main
    # installation and run the same command.
    name: lunarpaper-publisher
    services:
    publisher:
    image: ${PUBLISHER_IMAGE:-ghcr.io/lunarpaper/publisher}:${LUNAR_PAPER_VERSION:?Set LUNAR_PAPER_VERSION in .env}
    restart: unless-stopped
    ports:
    - "${PUBLISHER_BIND_ADDRESS:-0.0.0.0}:${PUBLISHER_HOST_PORT:-8081}:8080"
    environment:
    NODE_ENV: production
    # The main Lunar Paper address. It must be the same as its APP_URL.
    MAIN_APP_URL: ${MAIN_APP_URL:?Set MAIN_APP_URL in .env}
    PUBLISHER_INSTANCE_ID: ${PUBLISHER_INSTANCE_ID:?Set PUBLISHER_INSTANCE_ID in .env}
    API_KEY: ${API_KEY:?Set API_KEY in .env}
    PUBLISHER_BROKER_TOKEN: ${PUBLISHER_BROKER_TOKEN:?Set PUBLISHER_BROKER_TOKEN in .env}
    ASSET_ENCRYPTION_KEY: ${ASSET_ENCRYPTION_KEY:?Set ASSET_ENCRYPTION_KEY in .env}
    EMAIL_TRACKING_SECRET: ${EMAIL_TRACKING_SECRET:?Set EMAIL_TRACKING_SECRET in .env}
    # 0 when people connect directly, 1 behind a reverse proxy.
    PUBLISHER_TRUST_PROXY_HOPS: ${PUBLISHER_TRUST_PROXY_HOPS:-0}
    volumes:
    - "${PUBLISHER_DATA:-publisher-data}:/app/data"
    healthcheck:
    test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://127.0.0.1:8080/health"]
    interval: 30s
    timeout: 10s
    retries: 3
    start_period: 40s
    volumes:
    publisher-data:
  4. Validate and start the publisher. Validation does not print secrets.

    Terminal window
    docker compose config --quiet
    if ($LASTEXITCODE -eq 0) {
    docker compose pull
    if ($LASTEXITCODE -eq 0) { docker compose up -d --wait }
    }

    Open the publisher address you registered after startup succeeds. This is different from MAIN_APP_URL, which points back to the main application.

  5. Check the connection. In Publisher Inventory, select Health Check on the new publisher. Lunar Paper reports whether it can reach the publishing instance.

Next, allow services to publish to it and publish something. See Control what gets published and Publish, submit, and recover.

Connect an instance that is already running

Section titled “Connect an instance that is already running”

A publishing instance that is already running can be registered with the settings it already has. Its .env file and the content it stores stay as they are.

  1. Make sure the main installation can reach the instance’s address. An address on your own network must be listed in PUBLISHER_PRIVATE_TARGETS, as described in Allow an address on your own network.

  2. In Lunar Paper, open Administration → Publisher Services → Publisher Inventory and select Connect existing instance.

  3. Enter a name and the instance’s address. Then copy PUBLISHER_INSTANCE_ID, API_KEY and PUBLISHER_BROKER_TOKEN from the instance’s .env file, and select Connect.

    Lunar Paper asks the instance to confirm the instance ID and API key, and keeps the publisher only if the instance accepts them. It cannot check the broker token, so copy it exactly. If it is wrong, form submissions from the instance fail.

  4. Lunar Paper shows the settings it now uses for the instance. If the instance’s MAIN_APP_URL is not the main installation’s APP_URL, or its LUNAR_PAPER_VERSION is different, change them in the instance’s .env file and repeat the validation, pull and startup commands in its folder. Keep PUBLISHER_IMAGE as a repository without a tag and preserve the project name, connection settings and storage.

Keep the instance’s own ASSET_ENCRYPTION_KEY and EMAIL_TRACKING_SECRET. Lunar Paper does not need them, and a new ASSET_ENCRYPTION_KEY means publishing again every form that shows data from a dataset.

Content the instance holds from another installation does not appear under View Assets. Publish it again from this installation to manage it here.

To show published content to anyone on the internet, give the publishing instance a domain and put a reverse proxy with HTTPS in front of port 8081, such as Caddy or Nginx.

  1. Point the domain at the server, and set up the reverse proxy to forward to port 8081. The proxy requirements are the same as for your own reverse proxy.
  2. In the publishing instance’s .env, add PUBLISHER_TRUST_PROXY_HOPS=1, and run docker compose up -d --wait.
  3. Register the publishing instance with its https:// address.

Lunar Paper must be able to reach that address, and the publishing instance must still be able to reach MAIN_APP_URL.

Keep the publishing instance on the same version as the main installation. After you upgrade Lunar Paper, change LUNAR_PAPER_VERSION in the publishing instance’s .env to the same version and repeat the validation, pull and startup commands in its folder. Keep PUBLISHER_IMAGE as a repository without a tag and preserve the project name, connection settings and storage.

Published content is stored in the lunarpaper-publisher_publisher-data volume by default. A custom COMPOSE_PROJECT_NAME changes its prefix; a host folder selected with PUBLISHER_DATA needs a filesystem backup instead. Back up the selected storage and the .env file. If the volume is lost, publish each item again.

Both actions are in Publisher Inventory, and both change only this Lunar Paper installation, never the publishing instance itself. The Internal Publisher is part of Lunar Paper, so it can’t be edited, disabled or deleted. Every change to a publisher is recorded in Administration → Security & Access → Audit Log.

Disable takes a publisher out of use but keeps it registered. While it is disabled:

  • Services can’t publish anything new to it, and publishing an item again doesn’t update the copy the instance already holds.
  • Lunar Paper refuses everything the instance sends back, so submissions to the forms on it fail.
  • Content already published to it stays there, and people can still open it. To take an item down, deselect the publisher in the item’s Publishing locations.

Select Enable to put it back in use. Publish an item again to update its copy on the instance.

Delete removes the publisher from Lunar Paper. Lunar Paper first lists the items published to it and checks whether it can reach the instance:

  • If it can, it removes those items from the instance, then deletes the publisher. If an item can’t be removed, the publisher is kept so you can try again.
  • If it can’t, the items stay on the instance and remain public. Lunar Paper deletes the publisher only after you confirm. If you no longer need the instance, run docker compose down --volumes in its folder to delete it and everything it holds.

To use the instance again after deleting its publisher, connect it with Connect existing instance.

  • Create Publisher shows “Failed to create publisher”. Check the address. An address on your own network must be listed in PUBLISHER_PRIVATE_TARGETS exactly as host:port, as described in Allow an address on your own network, and the main installation must have been restarted since. An address on the internet must start with https://.
  • Connect existing instance says the address is not valid. Enter one port: the port the server exposes, such as http://192.168.1.20:8081. When Docker maps that port to the container’s port 8080, leave out the 8080.
  • Connect existing instance says the address is on a private network. Add it as host:port to PUBLISHER_PRIVATE_TARGETS, as described in Allow an address on your own network. Nothing changes on the publishing instance itself.
  • Connect existing instance says another publisher already uses this API key. A publisher registered earlier for the same instance holds that key. Delete that publisher first, then connect again.
  • Connect existing instance says the instance rejected the instance ID or API key. Copy PUBLISHER_INSTANCE_ID and API_KEY again from the instance’s .env file. If the instance was restarted with new values, use those.
  • Connect existing instance says the address did not answer like a publishing instance. Check that the address is the publishing instance’s, usually on port 8081, and not the main installation’s.
  • The publishing instance keeps restarting. Run docker compose logs publisher. The usual cause is a missing or shortened setting in .env. Show the settings again with Connection settings.
  • Forms show, but submissions never arrive. MAIN_APP_URL must be exactly the main installation’s APP_URL, and the publishing instance must be able to reach it.
  • You lost the settings. Connection settings shows everything except ASSET_ENCRYPTION_KEY and EMAIL_TRACKING_SECRET. If those are lost too, generate new values with openssl rand -hex 32, and publish again any forms that show data from a dataset.