Install with Docker Compose and HTTPS
This guide is the Docker Compose installation using the same configuration plus Caddy, a web server that serves Lunar Paper over HTTPS and looks after the certificate for you. Use this guide if you want encrypted access across a network, a public domain, or the camera, microphone and clipboard features that browsers only allow over HTTPS.
Caddy listens on ports 80 and 443, and Lunar Paper is only reachable through Caddy. If you already run a reverse proxy, see Use your own reverse proxy instead.
Choose your address and certificate
Section titled “Choose your address and certificate”| Internal name | Public domain | |
|---|---|---|
| Example | https://paper.internal |
https://paper.example.com |
| Certificate | From Caddy’s own certificate authority | Free, from Let’s Encrypt |
| Needs | Each device trusts Caddy’s certificate once | A public DNS record, and ports 80 and 443 open to the internet |
CADDY_TLS_DIRECTIVE |
tls internal |
tls you@example.com |
The .internal ending is reserved for private networks.
Before you start
Section titled “Before you start”- Install Docker, and make sure ports 80 and 443 are free.
- Linux and macOS need OpenSSL for the secret generator.
- Make the name point at the server with a DNS record. For a small setup, you
can instead add a line like
192.168.1.20 paper.internalto the hosts file on each device (C:\Windows\System32\drivers\etc\hostsor/etc/hosts). - Choose the platform release in
LUNAR_PAPER_VERSION. These examples target 0.8.1 in lunarpaper/lunarpaper.
Running multiple instances
Keep each installation in a separate folder with its own generated .env and
storage. These commands explicitly use --project-name lunarpaper, which takes
precedence over COMPOSE_PROJECT_NAME. For a fresh second instance, replace
lunarpaper after every --project-name with a previously unused name, such as
lunarpaper-test. Keep that name for future starts, backups and upgrades.
Use a distinct COOKIE_NAMESPACE if instances share a browser hostname.
Each Caddy instance needs its own available host ports. If ports 80 and 443 are
already occupied, use the basic Compose guide’s multiple-instance settings
and put the additional instance behind your
existing reverse proxy. Changing HOST_PORT does not change
Caddy’s ports in this HTTPS configuration.
Default named volumes are separated by project name. Custom host storage folders
must also be different. Reusing a project name or storage path can reuse an old
database; it does not become fresh when you generate another .env.
Install
Section titled “Install”-
Create the installation folder.
Terminal window mkdir lunarpapercd lunarpaperYour private
.envbelongs in this folder. Step 4 creates the configuration files from the reference in these docs. -
Generate
.env. Paste this into your terminal to create the file and generate its secrets. An existing.envis kept unchanged.Terminal window (set -euCif [ -e .env ] || [ -L .env ]; thenprintf '%s\n' 'Kept existing .env unchanged, including its settings and keys.'exit 0fiumask 077content=$(while IFS= read -r line; doif [[ "$line" == *'__LUNAR_PAPER_RANDOM_KEY__'* ]]; thenkey=$(openssl rand -hex 32) || exit 1[[ "$key" =~ ^[0-9a-f]{64}$ ]] || exit 1line="${line/__LUNAR_PAPER_RANDOM_KEY__/$key}"fiprintf '%s\n' "$line"done <<'LUNAR_PAPER_ENV'# Lunar Paper settings. Keep this file private and back it up with your data:# it holds the keys that protect everything Lunar Paper stores.# Platform release. PostgreSQL is versioned independently.LUNAR_PAPER_VERSION=0.8.1# Optional image repository override. Do not include a tag; LUNAR_PAPER_VERSION supplies it.# LUNAR_PAPER_IMAGE=ghcr.io/lunarpaper/platform# Optional cookie isolation for another instance on the same hostname.# COOKIE_NAMESPACE=lunarpaper_test# The address people type into their browser, with no slash at the end.# For example http://192.168.1.20:3000APP_URL=https://paper.internal# HTTPS certificate: use "tls internal" for your own network,# or "tls you@example.com" for a public domain.CADDY_TLS_DIRECTIVE=tls internal# Storage: keep these named-volume defaults, or use absolute host folder paths.# Changing a value does not move existing data.POSTGRES_DATA=postgres-dataREDIS_DATA=redis-dataUPLOADS_DATA=uploads-dataSEAWEEDFS_DATA=seaweedfs-data# Database usernames: defaults below; optionally override before first setup.# Custom names: start with a lowercase letter, then lowercase letters, digits or# underscores (max 63 characters). Use three distinct names. Keep app_* and pg_*# prefixes, postgres, public, svc_datalayer and reporting reserved for the defaults/system.PGUSER=postgresAPP_PLATFORM_USER=app_platformAPP_INTAKE_USER=app_intake# The first administrator. The password needs 12 or more characters, including# an upper-case letter, a lower-case letter, a number and a symbol.SYSTEM_ADMIN_USERNAME=you@example.comSYSTEM_ADMIN_PASSWORD=replace-with-your-password# Generated keys and passwords. Never change them after the first start:# some of them unlock data that is already encrypted.SETUP_KEY=__LUNAR_PAPER_RANDOM_KEY__# Database passwords are generated independently for the usernames above.PGPASSWORD=__LUNAR_PAPER_RANDOM_KEY__APP_PLATFORM_PASSWORD=__LUNAR_PAPER_RANDOM_KEY__APP_INTAKE_PASSWORD=__LUNAR_PAPER_RANDOM_KEY__REDIS_PASSWORD=__LUNAR_PAPER_RANDOM_KEY__SESSION_SECRET=__LUNAR_PAPER_RANDOM_KEY__ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__INTERNAL_API_TOKEN=__LUNAR_PAPER_RANDOM_KEY__AI_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__MFA_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__SSO_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__DATA_SOURCE_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__WORKFLOW_CREDENTIAL_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__PUBLISHER_CREDENTIAL_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__EMAIL_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__ADDRESS_PROVIDER_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__EMAIL_TRACKING_SECRET=__LUNAR_PAPER_RANDOM_KEY__PDF_EXPORT_SECRET=__LUNAR_PAPER_RANDOM_KEY__FORM_FILE_ENCRYPTION_KEYS={"v1":"__LUNAR_PAPER_RANDOM_KEY__"}LUNAR_PAPER_ENV)printf '%s\n' "$content" > .envprintf '%s\n' 'Created .env. Open it and replace the example address and administrator settings.')Terminal window & {$ErrorActionPreference = 'Stop'if (Test-Path -LiteralPath .env) {Write-Host 'Kept existing .env unchanged, including its settings and keys.'return}$template = @'# Lunar Paper settings. Keep this file private and back it up with your data:# it holds the keys that protect everything Lunar Paper stores.# Platform release. PostgreSQL is versioned independently.LUNAR_PAPER_VERSION=0.8.1# Optional image repository override. Do not include a tag; LUNAR_PAPER_VERSION supplies it.# LUNAR_PAPER_IMAGE=ghcr.io/lunarpaper/platform# Optional cookie isolation for another instance on the same hostname.# COOKIE_NAMESPACE=lunarpaper_test# The address people type into their browser, with no slash at the end.# For example http://192.168.1.20:3000APP_URL=https://paper.internal# HTTPS certificate: use "tls internal" for your own network,# or "tls you@example.com" for a public domain.CADDY_TLS_DIRECTIVE=tls internal# Storage: keep these named-volume defaults, or use absolute host folder paths.# Changing a value does not move existing data.POSTGRES_DATA=postgres-dataREDIS_DATA=redis-dataUPLOADS_DATA=uploads-dataSEAWEEDFS_DATA=seaweedfs-data# Database usernames: defaults below; optionally override before first setup.# Custom names: start with a lowercase letter, then lowercase letters, digits or# underscores (max 63 characters). Use three distinct names. Keep app_* and pg_*# prefixes, postgres, public, svc_datalayer and reporting reserved for the defaults/system.PGUSER=postgresAPP_PLATFORM_USER=app_platformAPP_INTAKE_USER=app_intake# The first administrator. The password needs 12 or more characters, including# an upper-case letter, a lower-case letter, a number and a symbol.SYSTEM_ADMIN_USERNAME=you@example.comSYSTEM_ADMIN_PASSWORD=replace-with-your-password# Generated keys and passwords. Never change them after the first start:# some of them unlock data that is already encrypted.SETUP_KEY=__LUNAR_PAPER_RANDOM_KEY__# Database passwords are generated independently for the usernames above.PGPASSWORD=__LUNAR_PAPER_RANDOM_KEY__APP_PLATFORM_PASSWORD=__LUNAR_PAPER_RANDOM_KEY__APP_INTAKE_PASSWORD=__LUNAR_PAPER_RANDOM_KEY__REDIS_PASSWORD=__LUNAR_PAPER_RANDOM_KEY__SESSION_SECRET=__LUNAR_PAPER_RANDOM_KEY__ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__INTERNAL_API_TOKEN=__LUNAR_PAPER_RANDOM_KEY__AI_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__MFA_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__SSO_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__DATA_SOURCE_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__WORKFLOW_CREDENTIAL_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__PUBLISHER_CREDENTIAL_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__EMAIL_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__ADDRESS_PROVIDER_ENCRYPTION_KEY=__LUNAR_PAPER_RANDOM_KEY__EMAIL_TRACKING_SECRET=__LUNAR_PAPER_RANDOM_KEY__PDF_EXPORT_SECRET=__LUNAR_PAPER_RANDOM_KEY__FORM_FILE_ENCRYPTION_KEYS={"v1":"__LUNAR_PAPER_RANDOM_KEY__"}'@$rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()try {$lines = foreach ($line in ($template -split "\r?\n")) {if ($line.Contains('__LUNAR_PAPER_RANDOM_KEY__')) {$bytes = New-Object byte[] 32$rng.GetBytes($bytes)$key = ([BitConverter]::ToString($bytes) -replace '-', '').ToLower()$line.Replace('__LUNAR_PAPER_RANDOM_KEY__', $key)} else { $line }}$content = ($lines -join [Environment]::NewLine) + [Environment]::NewLine$path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath('.env')$stream = [System.IO.File]::Open($path, [System.IO.FileMode]::CreateNew, [System.IO.FileAccess]::Write)try {$bytes = [System.Text.Encoding]::UTF8.GetBytes($content)$stream.Write($bytes, 0, $bytes.Length)} finally { $stream.Dispose() }} finally { $rng.Dispose() }Write-Host 'Created .env. Open it with: notepad .env, then replace the example address and administrator settings.'}Optionally create .env manually
Create
.envin your installation folder and paste this template. Replace every shortened value containing…with a separately generated 64-character hexadecimal secret, including the value insideFORM_FILE_ENCRYPTION_KEYS. Then edit the settings in step 3. Preserve existing secrets when updating an installation..env # Lunar Paper settings. Keep this file private and back it up with your data:# it holds the keys that protect everything Lunar Paper stores.# Platform release. PostgreSQL is versioned independently.LUNAR_PAPER_VERSION=0.8.1# Optional image repository override. Do not include a tag; LUNAR_PAPER_VERSION supplies it.# LUNAR_PAPER_IMAGE=ghcr.io/lunarpaper/platform# Optional cookie isolation for another instance on the same hostname.# COOKIE_NAMESPACE=lunarpaper_test# The address people type into their browser, with no slash at the end.# For example http://192.168.1.20:3000APP_URL=https://paper.internal# HTTPS certificate: use "tls internal" for your own network,# or "tls you@example.com" for a public domain.CADDY_TLS_DIRECTIVE=tls internal# Storage: keep these named-volume defaults, or use absolute host folder paths.# Changing a value does not move existing data.POSTGRES_DATA=postgres-dataREDIS_DATA=redis-dataUPLOADS_DATA=uploads-dataSEAWEEDFS_DATA=seaweedfs-data# Database usernames: defaults below; optionally override before first setup.# Custom names: start with a lowercase letter, then lowercase letters, digits or# underscores (max 63 characters). Use three distinct names. Keep app_* and pg_*# prefixes, postgres, public, svc_datalayer and reporting reserved for the defaults/system.PGUSER=postgresAPP_PLATFORM_USER=app_platformAPP_INTAKE_USER=app_intake# The first administrator. The password needs 12 or more characters, including# an upper-case letter, a lower-case letter, a number and a symbol.SYSTEM_ADMIN_USERNAME=you@example.comSYSTEM_ADMIN_PASSWORD=replace-with-your-password# Generated keys and passwords. Never change them after the first start:# some of them unlock data that is already encrypted.SETUP_KEY=20ff4c29…c39d# Database passwords are generated independently for the usernames above.PGPASSWORD=71d00f39…4054APP_PLATFORM_PASSWORD=3f99528e…6a07APP_INTAKE_PASSWORD=f451519c…f779REDIS_PASSWORD=4ee9a68a…db28SESSION_SECRET=8a3abd55…c4d0ENCRYPTION_KEY=9ee2c8be…b117INTERNAL_API_TOKEN=c310a8aa…1bfaAI_ENCRYPTION_KEY=a884e455…f6e6MFA_ENCRYPTION_KEY=23fe94af…7fcbSSO_ENCRYPTION_KEY=64b365fc…4672DATA_SOURCE_ENCRYPTION_KEY=19af350e…cadcWORKFLOW_CREDENTIAL_ENCRYPTION_KEY=5a4e076d…5f5bPUBLISHER_CREDENTIAL_ENCRYPTION_KEY=3b61c262…9a14EMAIL_ENCRYPTION_KEY=9a1c50c0…2dceADDRESS_PROVIDER_ENCRYPTION_KEY=ee02204d…135eEMAIL_TRACKING_SECRET=d295b034…7edcPDF_EXPORT_SECRET=e80d111f…8c3cFORM_FILE_ENCRYPTION_KEYS={"v1":"774453fa…fc60"} -
Edit your settings. Open
.envwithnotepad .envornano .envand set:APP_URL=https://paper.internalSYSTEM_ADMIN_USERNAME=you@example.comSYSTEM_ADMIN_PASSWORD=replace-with-your-passwordCADDY_TLS_DIRECTIVE=tls internalAPP_URLmust not include a port or end with a slash. For a public domain, usetls you@example.comwith your own email address. The password needs 12 or more characters, with upper case, lower case, a number and a symbol, and no spaces or quotes. Leave the generated lines unchanged.The storage settings default to the existing named volumes. To choose host folders before the first start, see Choose storage locations.
To customize database usernames before the first start, see Database usernames.
-
Generate the HTTPS files. Paste this into your terminal to create the files in
release. Existing files are kept unchanged.Terminal window (set -euCif [ -e release ] && [ ! -d release ]; thenprintf '%s\n' 'release must be a folder.' >&2exit 1fimkdir -p releaseif [ -e release/compose.yaml ] || [ -L release/compose.yaml ]; thenprintf '%s\n' 'Kept existing release/compose.yaml'elsecat > release/compose.yaml <<'LUNAR_PAPER_FILE'# Lunar Paper with official PostgreSQL. Select the platform version in your private environment.# Initialize or upgrade application schemas explicitly through protected web setup.services:lunarpaper:image: ${LUNAR_PAPER_IMAGE:-ghcr.io/lunarpaper/platform}:${LUNAR_PAPER_VERSION:?LUNAR_PAPER_VERSION is required}restart: unless-stoppedenvironment:NODE_ENV: productionPORT: "3000"PROCESS_ROLE: allCLUSTER_WORKERS: "1"# One browser URL controls origins and cookie security for HTTP or HTTPS.APP_URL: ${APP_URL:?APP_URL is required}API_URL: ${APP_URL:?APP_URL is required}ALLOWED_ORIGINS: ${APP_URL:?APP_URL is required}PUBLISHER_CALLBACK_ORIGINS: ${APP_URL:?APP_URL is required}# Keyed web setup and manual upgrades are fixed application behaviour.SETUP_KEY: ${SETUP_KEY:?SETUP_KEY is required}# Forward the same image selection to the app's release checks.LUNAR_PAPER_IMAGE: ${LUNAR_PAPER_IMAGE:-ghcr.io/lunarpaper/platform}:${LUNAR_PAPER_VERSION:?LUNAR_PAPER_VERSION is required}SYSTEM_ADMIN_USERNAME: ${SYSTEM_ADMIN_USERNAME:?SYSTEM_ADMIN_USERNAME is required}SYSTEM_ADMIN_PASSWORD: ${SYSTEM_ADMIN_PASSWORD:?SYSTEM_ADMIN_PASSWORD is required}SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required}COOKIE_NAMESPACE: ${COOKIE_NAMESPACE:-}ENCRYPTION_KEY: ${ENCRYPTION_KEY:?ENCRYPTION_KEY is required}WORKFLOW_CREDENTIAL_ENCRYPTION_KEY: ${WORKFLOW_CREDENTIAL_ENCRYPTION_KEY:?WORKFLOW_CREDENTIAL_ENCRYPTION_KEY is required}INTERNAL_API_TOKEN: ${INTERNAL_API_TOKEN:?INTERNAL_API_TOKEN is required}AI_ENCRYPTION_KEY: ${AI_ENCRYPTION_KEY:?AI_ENCRYPTION_KEY is required}MFA_ENCRYPTION_KEY: ${MFA_ENCRYPTION_KEY:?MFA_ENCRYPTION_KEY is required}SSO_ENCRYPTION_KEY: ${SSO_ENCRYPTION_KEY:?SSO_ENCRYPTION_KEY is required}DATA_SOURCE_ENCRYPTION_KEY: ${DATA_SOURCE_ENCRYPTION_KEY:?DATA_SOURCE_ENCRYPTION_KEY is required}PUBLISHER_CREDENTIAL_ENCRYPTION_KEY: ${PUBLISHER_CREDENTIAL_ENCRYPTION_KEY:?PUBLISHER_CREDENTIAL_ENCRYPTION_KEY is required}PUBLISHER_PRIVATE_TARGETS: ${PUBLISHER_PRIVATE_TARGETS:-}EMAIL_ENCRYPTION_KEY: ${EMAIL_ENCRYPTION_KEY:?EMAIL_ENCRYPTION_KEY is required}ADDRESS_PROVIDER_ENCRYPTION_KEY: ${ADDRESS_PROVIDER_ENCRYPTION_KEY:?ADDRESS_PROVIDER_ENCRYPTION_KEY is required}EMAIL_TRACKING_SECRET: ${EMAIL_TRACKING_SECRET:?EMAIL_TRACKING_SECRET is required}PDF_EXPORT_SECRET: ${PDF_EXPORT_SECRET:?PDF_EXPORT_SECRET is required}FORM_FILE_ENCRYPTION_KEYS: ${FORM_FILE_ENCRYPTION_KEYS:?FORM_FILE_ENCRYPTION_KEYS is required}FORM_FILE_ENCRYPTION_ACTIVE_KEY_ID: v1# Optional: Compose omits unset entries from the container environment.# See the commented remote-service example in .env.example.DATA_LAYER_ENABLED:DATA_LAYER_URL:DATA_LAYER_API_KEY:DATA_LAYER_ALLOWED_ENDPOINTS:# Two databases, each with a runtime URL and a separate setup/upgrade owner URL.PGHOST: postgresPGPORT: "5432"PGDATABASE: platformPGUSER: ${PGUSER:-postgres}PGPASSWORD: ${PGPASSWORD:?PGPASSWORD is required}APP_PLATFORM_USER: ${APP_PLATFORM_USER:-app_platform}APP_INTAKE_USER: ${APP_INTAKE_USER:-app_intake}DATABASE_URL: postgresql://${APP_PLATFORM_USER:-app_platform}:${APP_PLATFORM_PASSWORD:?APP_PLATFORM_PASSWORD is required}@postgres:5432/platformINTAKE_DATABASE_URL: postgresql://${APP_INTAKE_USER:-app_intake}:${APP_INTAKE_PASSWORD:?APP_INTAKE_PASSWORD is required}@postgres:5432/intakeAPP_PLATFORM_PASSWORD: ${APP_PLATFORM_PASSWORD:?APP_PLATFORM_PASSWORD is required}APP_INTAKE_PASSWORD: ${APP_INTAKE_PASSWORD:?APP_INTAKE_PASSWORD is required}MIGRATION_PLATFORM_DATABASE_URL: postgresql://${PGUSER:-postgres}:${PGPASSWORD:?PGPASSWORD is required}@postgres:5432/platformMIGRATION_INTAKE_DATABASE_URL: postgresql://${PGUSER:-postgres}:${PGPASSWORD:?PGPASSWORD is required}@postgres:5432/intakeAPP_DB_ROLE: "true"DB_POOL_MAX: "40"SESSION_STORE: redisREDIS_REQUIRED: "true"REDIS_HOST: redisREDIS_PORT: "6379"REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD is required}SEAWEEDFS_MASTER_URL: http://seaweedfs:9333SEAWEEDFS_FILER_URL: http://seaweedfs:8888SEAWEEDFS_VOLUME_URL: http://seaweedfs:8080volumes:- "${UPLOADS_DATA:-uploads-data}:/app/uploads"depends_on:postgres:condition: service_healthyredis:condition: service_healthyseaweedfs:condition: service_healthyhealthcheck:test:- CMD- node- -e- "fetch('http://127.0.0.1:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"interval: 15stimeout: 5sretries: 3start_period: 120snetworks:- edge- backendpostgres:image: ${POSTGRES_IMAGE:-postgres:17-alpine}restart: unless-stoppedenvironment:POSTGRES_DB: platformPOSTGRES_USER: ${PGUSER:-postgres}POSTGRES_PASSWORD: ${PGPASSWORD:?PGPASSWORD is required}command: ["postgres", "-c", "max_connections=200"]volumes:- "${POSTGRES_DATA:-postgres-data}:/var/lib/postgresql/data"healthcheck:test: ['CMD-SHELL', 'pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"']interval: 10stimeout: 5sretries: 12start_period: 30snetworks:- backendredis:image: redis:7-alpinerestart: unless-stoppedenvironment:REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD is required}command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:?REDIS_PASSWORD is required}"]volumes:- "${REDIS_DATA:-redis-data}:/data"healthcheck:test: ["CMD-SHELL", "REDISCLI_AUTH=\"$${REDIS_PASSWORD}\" redis-cli ping | grep -q PONG"]interval: 10stimeout: 5sretries: 12start_period: 10snetworks:- backend# Master, volume server and filer together for a single-server installation.seaweedfs:image: chrislusf/seaweedfs:4.47restart: unless-stoppedworking_dir: /datacommand:- server- -ip=seaweedfs- -ip.bind=0.0.0.0- -dir=/data- -master.volumeSizeLimitMB=1024- -volume.max=0- -filervolumes:- "${SEAWEEDFS_DATA:-seaweedfs-data}:/data"- ./filer.toml:/etc/seaweedfs/filer.toml:rohealthcheck:test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:9333/cluster/status && wget -q -O /dev/null http://127.0.0.1:8888/"]interval: 10stimeout: 5sretries: 12start_period: 30snetworks:- backendvolumes:postgres-data:redis-data:uploads-data:seaweedfs-data:networks:edge:backend:internal: trueLUNAR_PAPER_FILEprintf '%s\n' 'Created release/compose.yaml'fiif [ -e release/compose.https.yaml ] || [ -L release/compose.https.yaml ]; thenprintf '%s\n' 'Kept existing release/compose.https.yaml'elsecat > release/compose.https.yaml <<'LUNAR_PAPER_FILE'# Use with compose.yaml; set APP_URL to https://your-internal-hostname.services:lunarpaper:environment:TRUST_PROXY_HOPS: "1"caddy:image: caddy:2-alpinerestart: unless-stoppedenvironment:APP_URL: ${APP_URL:?APP_URL is required}CADDY_TLS_DIRECTIVE: ${CADDY_TLS_DIRECTIVE-tls internal}ports:- "80:80"- "443:443"- "443:443/udp"volumes:- ./Caddyfile:/etc/caddy/Caddyfile:ro- caddy-data:/data- caddy-config:/configdepends_on:lunarpaper:condition: service_healthynetworks:- edgevolumes:caddy-data:caddy-config:LUNAR_PAPER_FILEprintf '%s\n' 'Created release/compose.https.yaml'fiif [ -e release/filer.toml ] || [ -L release/filer.toml ]; thenprintf '%s\n' 'Kept existing release/filer.toml'elsecat > release/filer.toml <<'LUNAR_PAPER_FILE'# Persist the file directory/index alongside the file contents in the data volume.[leveldb2]enabled = truedir = "/data/filerldb2"LUNAR_PAPER_FILEprintf '%s\n' 'Created release/filer.toml'fiif [ -e release/Caddyfile ] || [ -L release/Caddyfile ]; thenprintf '%s\n' 'Kept existing release/Caddyfile'elsecat > release/Caddyfile <<'LUNAR_PAPER_FILE'{$APP_URL} {{$CADDY_TLS_DIRECTIVE}encode zstd gzipreverse_proxy lunarpaper:3000header -Server}LUNAR_PAPER_FILEprintf '%s\n' 'Created release/Caddyfile'fi)Terminal window & {$ErrorActionPreference = 'Stop'if (Test-Path -LiteralPath ./release) {if (-not (Test-Path -LiteralPath ./release -PathType Container)) { throw 'release must be a folder.' }} else {New-Item -ItemType Directory -Path ./release | Out-Null}$files = [ordered]@{'compose.yaml' = @'# Lunar Paper with official PostgreSQL. Select the platform version in your private environment.# Initialize or upgrade application schemas explicitly through protected web setup.services:lunarpaper:image: ${LUNAR_PAPER_IMAGE:-ghcr.io/lunarpaper/platform}:${LUNAR_PAPER_VERSION:?LUNAR_PAPER_VERSION is required}restart: unless-stoppedenvironment:NODE_ENV: productionPORT: "3000"PROCESS_ROLE: allCLUSTER_WORKERS: "1"# One browser URL controls origins and cookie security for HTTP or HTTPS.APP_URL: ${APP_URL:?APP_URL is required}API_URL: ${APP_URL:?APP_URL is required}ALLOWED_ORIGINS: ${APP_URL:?APP_URL is required}PUBLISHER_CALLBACK_ORIGINS: ${APP_URL:?APP_URL is required}# Keyed web setup and manual upgrades are fixed application behaviour.SETUP_KEY: ${SETUP_KEY:?SETUP_KEY is required}# Forward the same image selection to the app's release checks.LUNAR_PAPER_IMAGE: ${LUNAR_PAPER_IMAGE:-ghcr.io/lunarpaper/platform}:${LUNAR_PAPER_VERSION:?LUNAR_PAPER_VERSION is required}SYSTEM_ADMIN_USERNAME: ${SYSTEM_ADMIN_USERNAME:?SYSTEM_ADMIN_USERNAME is required}SYSTEM_ADMIN_PASSWORD: ${SYSTEM_ADMIN_PASSWORD:?SYSTEM_ADMIN_PASSWORD is required}SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required}COOKIE_NAMESPACE: ${COOKIE_NAMESPACE:-}ENCRYPTION_KEY: ${ENCRYPTION_KEY:?ENCRYPTION_KEY is required}WORKFLOW_CREDENTIAL_ENCRYPTION_KEY: ${WORKFLOW_CREDENTIAL_ENCRYPTION_KEY:?WORKFLOW_CREDENTIAL_ENCRYPTION_KEY is required}INTERNAL_API_TOKEN: ${INTERNAL_API_TOKEN:?INTERNAL_API_TOKEN is required}AI_ENCRYPTION_KEY: ${AI_ENCRYPTION_KEY:?AI_ENCRYPTION_KEY is required}MFA_ENCRYPTION_KEY: ${MFA_ENCRYPTION_KEY:?MFA_ENCRYPTION_KEY is required}SSO_ENCRYPTION_KEY: ${SSO_ENCRYPTION_KEY:?SSO_ENCRYPTION_KEY is required}DATA_SOURCE_ENCRYPTION_KEY: ${DATA_SOURCE_ENCRYPTION_KEY:?DATA_SOURCE_ENCRYPTION_KEY is required}PUBLISHER_CREDENTIAL_ENCRYPTION_KEY: ${PUBLISHER_CREDENTIAL_ENCRYPTION_KEY:?PUBLISHER_CREDENTIAL_ENCRYPTION_KEY is required}PUBLISHER_PRIVATE_TARGETS: ${PUBLISHER_PRIVATE_TARGETS:-}EMAIL_ENCRYPTION_KEY: ${EMAIL_ENCRYPTION_KEY:?EMAIL_ENCRYPTION_KEY is required}ADDRESS_PROVIDER_ENCRYPTION_KEY: ${ADDRESS_PROVIDER_ENCRYPTION_KEY:?ADDRESS_PROVIDER_ENCRYPTION_KEY is required}EMAIL_TRACKING_SECRET: ${EMAIL_TRACKING_SECRET:?EMAIL_TRACKING_SECRET is required}PDF_EXPORT_SECRET: ${PDF_EXPORT_SECRET:?PDF_EXPORT_SECRET is required}FORM_FILE_ENCRYPTION_KEYS: ${FORM_FILE_ENCRYPTION_KEYS:?FORM_FILE_ENCRYPTION_KEYS is required}FORM_FILE_ENCRYPTION_ACTIVE_KEY_ID: v1# Optional: Compose omits unset entries from the container environment.# See the commented remote-service example in .env.example.DATA_LAYER_ENABLED:DATA_LAYER_URL:DATA_LAYER_API_KEY:DATA_LAYER_ALLOWED_ENDPOINTS:# Two databases, each with a runtime URL and a separate setup/upgrade owner URL.PGHOST: postgresPGPORT: "5432"PGDATABASE: platformPGUSER: ${PGUSER:-postgres}PGPASSWORD: ${PGPASSWORD:?PGPASSWORD is required}APP_PLATFORM_USER: ${APP_PLATFORM_USER:-app_platform}APP_INTAKE_USER: ${APP_INTAKE_USER:-app_intake}DATABASE_URL: postgresql://${APP_PLATFORM_USER:-app_platform}:${APP_PLATFORM_PASSWORD:?APP_PLATFORM_PASSWORD is required}@postgres:5432/platformINTAKE_DATABASE_URL: postgresql://${APP_INTAKE_USER:-app_intake}:${APP_INTAKE_PASSWORD:?APP_INTAKE_PASSWORD is required}@postgres:5432/intakeAPP_PLATFORM_PASSWORD: ${APP_PLATFORM_PASSWORD:?APP_PLATFORM_PASSWORD is required}APP_INTAKE_PASSWORD: ${APP_INTAKE_PASSWORD:?APP_INTAKE_PASSWORD is required}MIGRATION_PLATFORM_DATABASE_URL: postgresql://${PGUSER:-postgres}:${PGPASSWORD:?PGPASSWORD is required}@postgres:5432/platformMIGRATION_INTAKE_DATABASE_URL: postgresql://${PGUSER:-postgres}:${PGPASSWORD:?PGPASSWORD is required}@postgres:5432/intakeAPP_DB_ROLE: "true"DB_POOL_MAX: "40"SESSION_STORE: redisREDIS_REQUIRED: "true"REDIS_HOST: redisREDIS_PORT: "6379"REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD is required}SEAWEEDFS_MASTER_URL: http://seaweedfs:9333SEAWEEDFS_FILER_URL: http://seaweedfs:8888SEAWEEDFS_VOLUME_URL: http://seaweedfs:8080volumes:- "${UPLOADS_DATA:-uploads-data}:/app/uploads"depends_on:postgres:condition: service_healthyredis:condition: service_healthyseaweedfs:condition: service_healthyhealthcheck:test:- CMD- node- -e- "fetch('http://127.0.0.1:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"interval: 15stimeout: 5sretries: 3start_period: 120snetworks:- edge- backendpostgres:image: ${POSTGRES_IMAGE:-postgres:17-alpine}restart: unless-stoppedenvironment:POSTGRES_DB: platformPOSTGRES_USER: ${PGUSER:-postgres}POSTGRES_PASSWORD: ${PGPASSWORD:?PGPASSWORD is required}command: ["postgres", "-c", "max_connections=200"]volumes:- "${POSTGRES_DATA:-postgres-data}:/var/lib/postgresql/data"healthcheck:test: ['CMD-SHELL', 'pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"']interval: 10stimeout: 5sretries: 12start_period: 30snetworks:- backendredis:image: redis:7-alpinerestart: unless-stoppedenvironment:REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD is required}command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:?REDIS_PASSWORD is required}"]volumes:- "${REDIS_DATA:-redis-data}:/data"healthcheck:test: ["CMD-SHELL", "REDISCLI_AUTH=\"$${REDIS_PASSWORD}\" redis-cli ping | grep -q PONG"]interval: 10stimeout: 5sretries: 12start_period: 10snetworks:- backend# Master, volume server and filer together for a single-server installation.seaweedfs:image: chrislusf/seaweedfs:4.47restart: unless-stoppedworking_dir: /datacommand:- server- -ip=seaweedfs- -ip.bind=0.0.0.0- -dir=/data- -master.volumeSizeLimitMB=1024- -volume.max=0- -filervolumes:- "${SEAWEEDFS_DATA:-seaweedfs-data}:/data"- ./filer.toml:/etc/seaweedfs/filer.toml:rohealthcheck:test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:9333/cluster/status && wget -q -O /dev/null http://127.0.0.1:8888/"]interval: 10stimeout: 5sretries: 12start_period: 30snetworks:- backendvolumes:postgres-data:redis-data:uploads-data:seaweedfs-data:networks:edge:backend:internal: true'@'compose.https.yaml' = @'# Use with compose.yaml; set APP_URL to https://your-internal-hostname.services:lunarpaper:environment:TRUST_PROXY_HOPS: "1"caddy:image: caddy:2-alpinerestart: unless-stoppedenvironment:APP_URL: ${APP_URL:?APP_URL is required}CADDY_TLS_DIRECTIVE: ${CADDY_TLS_DIRECTIVE-tls internal}ports:- "80:80"- "443:443"- "443:443/udp"volumes:- ./Caddyfile:/etc/caddy/Caddyfile:ro- caddy-data:/data- caddy-config:/configdepends_on:lunarpaper:condition: service_healthynetworks:- edgevolumes:caddy-data:caddy-config:'@'filer.toml' = @'# Persist the file directory/index alongside the file contents in the data volume.[leveldb2]enabled = truedir = "/data/filerldb2"'@'Caddyfile' = @'{$APP_URL} {{$CADDY_TLS_DIRECTIVE}encode zstd gzipreverse_proxy lunarpaper:3000header -Server}'@}foreach ($file in $files.GetEnumerator()) {$destination = Join-Path release $file.Keyif (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 the HTTPS files manually
Create a
releasefolder beside.env. Save each file below with its shown filename. Keep the${VARIABLE}expressions; Compose fills them from.env.release/compose.yaml # Lunar Paper with official PostgreSQL. Select the platform version in your private environment.# Initialize or upgrade application schemas explicitly through protected web setup.services:lunarpaper:image: ${LUNAR_PAPER_IMAGE:-ghcr.io/lunarpaper/platform}:${LUNAR_PAPER_VERSION:?LUNAR_PAPER_VERSION is required}restart: unless-stoppedenvironment:NODE_ENV: productionPORT: "3000"PROCESS_ROLE: allCLUSTER_WORKERS: "1"# One browser URL controls origins and cookie security for HTTP or HTTPS.APP_URL: ${APP_URL:?APP_URL is required}API_URL: ${APP_URL:?APP_URL is required}ALLOWED_ORIGINS: ${APP_URL:?APP_URL is required}PUBLISHER_CALLBACK_ORIGINS: ${APP_URL:?APP_URL is required}# Keyed web setup and manual upgrades are fixed application behaviour.SETUP_KEY: ${SETUP_KEY:?SETUP_KEY is required}# Forward the same image selection to the app's release checks.LUNAR_PAPER_IMAGE: ${LUNAR_PAPER_IMAGE:-ghcr.io/lunarpaper/platform}:${LUNAR_PAPER_VERSION:?LUNAR_PAPER_VERSION is required}SYSTEM_ADMIN_USERNAME: ${SYSTEM_ADMIN_USERNAME:?SYSTEM_ADMIN_USERNAME is required}SYSTEM_ADMIN_PASSWORD: ${SYSTEM_ADMIN_PASSWORD:?SYSTEM_ADMIN_PASSWORD is required}SESSION_SECRET: ${SESSION_SECRET:?SESSION_SECRET is required}COOKIE_NAMESPACE: ${COOKIE_NAMESPACE:-}ENCRYPTION_KEY: ${ENCRYPTION_KEY:?ENCRYPTION_KEY is required}WORKFLOW_CREDENTIAL_ENCRYPTION_KEY: ${WORKFLOW_CREDENTIAL_ENCRYPTION_KEY:?WORKFLOW_CREDENTIAL_ENCRYPTION_KEY is required}INTERNAL_API_TOKEN: ${INTERNAL_API_TOKEN:?INTERNAL_API_TOKEN is required}AI_ENCRYPTION_KEY: ${AI_ENCRYPTION_KEY:?AI_ENCRYPTION_KEY is required}MFA_ENCRYPTION_KEY: ${MFA_ENCRYPTION_KEY:?MFA_ENCRYPTION_KEY is required}SSO_ENCRYPTION_KEY: ${SSO_ENCRYPTION_KEY:?SSO_ENCRYPTION_KEY is required}DATA_SOURCE_ENCRYPTION_KEY: ${DATA_SOURCE_ENCRYPTION_KEY:?DATA_SOURCE_ENCRYPTION_KEY is required}PUBLISHER_CREDENTIAL_ENCRYPTION_KEY: ${PUBLISHER_CREDENTIAL_ENCRYPTION_KEY:?PUBLISHER_CREDENTIAL_ENCRYPTION_KEY is required}PUBLISHER_PRIVATE_TARGETS: ${PUBLISHER_PRIVATE_TARGETS:-}EMAIL_ENCRYPTION_KEY: ${EMAIL_ENCRYPTION_KEY:?EMAIL_ENCRYPTION_KEY is required}ADDRESS_PROVIDER_ENCRYPTION_KEY: ${ADDRESS_PROVIDER_ENCRYPTION_KEY:?ADDRESS_PROVIDER_ENCRYPTION_KEY is required}EMAIL_TRACKING_SECRET: ${EMAIL_TRACKING_SECRET:?EMAIL_TRACKING_SECRET is required}PDF_EXPORT_SECRET: ${PDF_EXPORT_SECRET:?PDF_EXPORT_SECRET is required}FORM_FILE_ENCRYPTION_KEYS: ${FORM_FILE_ENCRYPTION_KEYS:?FORM_FILE_ENCRYPTION_KEYS is required}FORM_FILE_ENCRYPTION_ACTIVE_KEY_ID: v1# Optional: Compose omits unset entries from the container environment.# See the commented remote-service example in .env.example.DATA_LAYER_ENABLED:DATA_LAYER_URL:DATA_LAYER_API_KEY:DATA_LAYER_ALLOWED_ENDPOINTS:# Two databases, each with a runtime URL and a separate setup/upgrade owner URL.PGHOST: postgresPGPORT: "5432"PGDATABASE: platformPGUSER: ${PGUSER:-postgres}PGPASSWORD: ${PGPASSWORD:?PGPASSWORD is required}APP_PLATFORM_USER: ${APP_PLATFORM_USER:-app_platform}APP_INTAKE_USER: ${APP_INTAKE_USER:-app_intake}DATABASE_URL: postgresql://${APP_PLATFORM_USER:-app_platform}:${APP_PLATFORM_PASSWORD:?APP_PLATFORM_PASSWORD is required}@postgres:5432/platformINTAKE_DATABASE_URL: postgresql://${APP_INTAKE_USER:-app_intake}:${APP_INTAKE_PASSWORD:?APP_INTAKE_PASSWORD is required}@postgres:5432/intakeAPP_PLATFORM_PASSWORD: ${APP_PLATFORM_PASSWORD:?APP_PLATFORM_PASSWORD is required}APP_INTAKE_PASSWORD: ${APP_INTAKE_PASSWORD:?APP_INTAKE_PASSWORD is required}MIGRATION_PLATFORM_DATABASE_URL: postgresql://${PGUSER:-postgres}:${PGPASSWORD:?PGPASSWORD is required}@postgres:5432/platformMIGRATION_INTAKE_DATABASE_URL: postgresql://${PGUSER:-postgres}:${PGPASSWORD:?PGPASSWORD is required}@postgres:5432/intakeAPP_DB_ROLE: "true"DB_POOL_MAX: "40"SESSION_STORE: redisREDIS_REQUIRED: "true"REDIS_HOST: redisREDIS_PORT: "6379"REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD is required}SEAWEEDFS_MASTER_URL: http://seaweedfs:9333SEAWEEDFS_FILER_URL: http://seaweedfs:8888SEAWEEDFS_VOLUME_URL: http://seaweedfs:8080volumes:- "${UPLOADS_DATA:-uploads-data}:/app/uploads"depends_on:postgres:condition: service_healthyredis:condition: service_healthyseaweedfs:condition: service_healthyhealthcheck:test:- CMD- node- -e- "fetch('http://127.0.0.1:3000/api/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"interval: 15stimeout: 5sretries: 3start_period: 120snetworks:- edge- backendpostgres:image: ${POSTGRES_IMAGE:-postgres:17-alpine}restart: unless-stoppedenvironment:POSTGRES_DB: platformPOSTGRES_USER: ${PGUSER:-postgres}POSTGRES_PASSWORD: ${PGPASSWORD:?PGPASSWORD is required}command: ["postgres", "-c", "max_connections=200"]volumes:- "${POSTGRES_DATA:-postgres-data}:/var/lib/postgresql/data"healthcheck:test: ['CMD-SHELL', 'pg_isready -U "$${POSTGRES_USER}" -d "$${POSTGRES_DB}"']interval: 10stimeout: 5sretries: 12start_period: 30snetworks:- backendredis:image: redis:7-alpinerestart: unless-stoppedenvironment:REDIS_PASSWORD: ${REDIS_PASSWORD:?REDIS_PASSWORD is required}command: ["redis-server", "--appendonly", "yes", "--requirepass", "${REDIS_PASSWORD:?REDIS_PASSWORD is required}"]volumes:- "${REDIS_DATA:-redis-data}:/data"healthcheck:test: ["CMD-SHELL", "REDISCLI_AUTH=\"$${REDIS_PASSWORD}\" redis-cli ping | grep -q PONG"]interval: 10stimeout: 5sretries: 12start_period: 10snetworks:- backend# Master, volume server and filer together for a single-server installation.seaweedfs:image: chrislusf/seaweedfs:4.47restart: unless-stoppedworking_dir: /datacommand:- server- -ip=seaweedfs- -ip.bind=0.0.0.0- -dir=/data- -master.volumeSizeLimitMB=1024- -volume.max=0- -filervolumes:- "${SEAWEEDFS_DATA:-seaweedfs-data}:/data"- ./filer.toml:/etc/seaweedfs/filer.toml:rohealthcheck:test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:9333/cluster/status && wget -q -O /dev/null http://127.0.0.1:8888/"]interval: 10stimeout: 5sretries: 12start_period: 30snetworks:- backendvolumes:postgres-data:redis-data:uploads-data:seaweedfs-data:networks:edge:backend:internal: truerelease/compose.https.yaml # Use with compose.yaml; set APP_URL to https://your-internal-hostname.services:lunarpaper:environment:TRUST_PROXY_HOPS: "1"caddy:image: caddy:2-alpinerestart: unless-stoppedenvironment:APP_URL: ${APP_URL:?APP_URL is required}CADDY_TLS_DIRECTIVE: ${CADDY_TLS_DIRECTIVE-tls internal}ports:- "80:80"- "443:443"- "443:443/udp"volumes:- ./Caddyfile:/etc/caddy/Caddyfile:ro- caddy-data:/data- caddy-config:/configdepends_on:lunarpaper:condition: service_healthynetworks:- edgevolumes:caddy-data:caddy-config:release/filer.toml # Persist the file directory/index alongside the file contents in the data volume.[leveldb2]enabled = truedir = "/data/filerldb2"release/Caddyfile {$APP_URL} {{$CADDY_TLS_DIRECTIVE}encode zstd gzipreverse_proxy lunarpaper:3000header -Server}The HTTPS access file publishes Caddy on ports 80 and 443 and keeps the application port private. Use exactly one access file; do not combine HTTP and HTTPS. PostgreSQL uses the official
postgres:17-alpineimage. -
Validate the configuration without printing secrets.
Terminal window docker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml config --quiet -
Pull the images and start Lunar Paper.
Terminal window docker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml pullif ($LASTEXITCODE -eq 0) {docker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml up -d --waitif ($LASTEXITCODE -eq 0) {docker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml exec -T lunarpaper printenv APP_URL}}Terminal window docker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml pull &&docker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml up -d --wait &&docker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml exec -T lunarpaper printenv APP_URLStartup prints your
APP_URLwhen the containers are healthy. Install the certificate below before opening it. Resolve any startup error before continuing. -
Trust Caddy’s certificate. Skip this step if you use a public domain. For an internal name, copy the certificate out of the container:
Terminal window docker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml cp caddy:/data/caddy/pki/authorities/local/root.crt lunarpaper-root.crtInstall
lunarpaper-root.crton every device that uses Lunar Paper. The file contains no secrets, so it is safe to copy around.Double-click the file, select Install Certificate, choose Local Machine, then Place all certificates in the following store, and pick Trusted Root Certification Authorities. Restart the browser.
Double-click the file and add it to the System keychain. Open the Caddy Local Authority certificate, expand Trust, and set When using this certificate to Always Trust.
Debian or Ubuntu:
Terminal window sudo cp lunarpaper-root.crt /usr/local/share/ca-certificates/lunarpaper-root.crtsudo update-ca-certificatesFedora or Red Hat:
Terminal window sudo cp lunarpaper-root.crt /etc/pki/ca-trust/source/anchors/sudo update-ca-trustIf a browser still shows a warning, import the file as an authority in that browser’s certificate settings.
Open the file on the device, install the profile under Settings > General > VPN & Device Management, then turn on full trust under Settings > General > About > Certificate Trust Settings.
Go to Settings > Security > Encryption & credentials > Install a certificate > CA certificate and choose the file. Menu names vary by manufacturer.
Firefox may use its own list of certificates. If it still shows a warning, import the file under Settings > Privacy & Security > Certificates > View Certificates > Authorities.
-
Open your
APP_URLand initialize. Unlock setup withSETUP_KEY, review the empty databases and explicitly start initialization. Wait for both databases and release activation to complete, then launch and sign in. See First-time setup.
If you see a maintenance page instead, follow Complete the database upgrade.
Everyday commands
Section titled “Everyday commands”Run these in the lunarpaper folder. Keep both HTTPS configuration files on
every command:
docker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml psdocker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml logs -f lunarpaperdocker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml stopdocker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml up -d --waitUpgrade
Section titled “Upgrade”-
Read the release notes and set
LUNAR_PAPER_VERSIONin.envto the published version.LUNAR_PAPER_IMAGEstays as the repository name without a tag; Compose appends the selected version automatically. -
Keep your configuration, project name, secrets and volumes. Validate, then replace only the platform container:
Terminal window docker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml config --quietdocker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml pull lunarpaperdocker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml up -d --wait --no-deps lunarpaper -
If a maintenance page appears, complete the database upgrade.
Check that it is running
Section titled “Check that it is running”docker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml ps should show caddy on ports 80 and 443, and the other four
containers as healthy. From a device that trusts the certificate,
https://<your address>/api/health/ready should load without an error.
Switch from plain Docker Compose
Section titled “Switch from plain Docker Compose”You can add HTTPS to an existing Docker Compose installation without losing
data. In your existing lunarpaper folder:
- Take a backup and keep the same configuration, project name, secrets and volumes.
- Change
APP_URLin.envto thehttps://address and add theCADDY_TLS_DIRECTIVEline. - Run the file-creation command in step 4 above to create the HTTPS configuration
in
release. If you customized your HTTPcompose.yaml, carry those changes intorelease/compose.yaml, except for its HTTPportsmapping. Keep the original project name to reuse the same data volumes. If you used a customCOMPOSE_PROJECT_NAME, replacelunarpaperafter--project-namein every HTTPS command with that name; the command-line option overrides.env. - Run the validation, pull and startup commands above. They explicitly select
the files in
release; your old HTTPcompose.yamlis no longer used. Use these HTTPS commands for future starts and upgrades too. - Trust Caddy’s certificate as in step 7.
Afterwards, update any bookmarks and single sign-on callback addresses to the new address.
Use an IP address instead of a name
Section titled “Use an IP address instead of a name”Set APP_URL to the IP address, such as https://192.168.1.20, and set
CADDY_TLS_DIRECTIVE to tls internal. Then add this to the top of the
Caddyfile:
{ default_sni 192.168.1.20}Browsers do not send a server name when they connect to an IP address, so this setting tells Caddy which certificate to use. Public certificate authorities do not issue certificates for private IP addresses.
Troubleshooting
Section titled “Troubleshooting”- Database login fails with
28P01. Existing volumes keep their original database passwords. Restore the matching.env; generating new secrets or recreating a container does not change stored passwords. For a separate fresh installation, use the multiple-instance guidance. - The certificate is not trusted. Install
lunarpaper-root.crton that device (step 7) and restart the browser. - Caddy cannot start because a port is in use. Another web server is using port 80 or 443. Stop it, or use your own reverse proxy.
- A public domain gets no certificate. Check the DNS record, and check that
ports 80 and 443 are reachable from the internet. Run
docker compose --project-name lunarpaper --env-file .env -f release/compose.yaml -f release/compose.https.yaml logs caddyto see the error. - Some devices cannot reach the name. Check that the name works on that
device, for example with
ping paper.internal.
Keep the lunarpaper_caddy-data volume. It holds the certificates and Caddy’s
certificate authority. If you lose it, every device has to trust a new
certificate.
