Sign inSign up

gotnonamedocker/memoryatlas-selfhost

By gotnonamedocker

•Updated about 9 hours ago

Your travel photos, mapped and organized on your own server. Self-hosted Memory Atlas.

Image
Content management system
0

89

gotnonamedocker/memoryatlas-selfhost repository overview

⁠Memory Atlas, self-hosted

Turn your travel photos into a map of your adventures — on your own hardware.

Memory Atlas reads the GPS and dates already inside your photos and videos and organizes them by place, visit and trip, with a map, a timeline and a trip journal on top. The organizing is automatic: you upload, and the atlas is built for you.

This image is the self-hosted edition: the whole application, running on your server, with nothing phoning home.

  • Three containers — the app, PostgreSQL/PostGIS, and S3-compatible object storage. No external database, cloud storage or account required.
  • Automatic organization — places and visits are inferred from EXIF: GPS, capture time, camera.
  • Photos and video — JPEG, HEIC, RAW, MP4/MOV; thumbnails, previews and HEIC conversion are done in the container.
  • Private by construction — no telemetry, no external calls, your data stays in your volumes.
  • Already multi-arch — linux/amd64 and linux/arm64 in one manifest list.

Documentation⁠ · Support⁠

⁠Quick start

Three files in one directory: compose.yaml (below), .env, and optionally a Caddyfile for HTTPS. Compose reads the first two by name.

# 1. Generate the five secrets, one per line
for k in DB_PASSWORD STORAGE_ACCESS_KEY STORAGE_SECRET_KEY JWT_SECRET SETUP_TOKEN; do
  printf '%s=%s\n' "$k" "$(openssl rand -hex 32)"
done

Put them in .env with the two addresses (see the table below):

APP_URL=http://localhost:8080
STORAGE_PUBLIC_ENDPOINT=localhost:8333
# 2. Start it
docker compose up -d

# 3. Open APP_URL/setup and paste SETUP_TOKEN to create the owner account

The first start takes about a minute: migrations run, the storage bucket is created. Until the first account exists, every address on the instance leads to the setup page.

name: memory-atlas-selfhost

services:
  postgres:
    image: postgis/postgis:17-3.5-alpine
    environment:
      POSTGRES_DB: memory_atlas
      POSTGRES_USER: memory_atlas
      POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U memory_atlas -d memory_atlas"]
      interval: 5s
      timeout: 5s
      retries: 12
    restart: unless-stopped

  storage:
    image: chrislusf/seaweedfs:latest
    command: "server -s3 -dir=/data"
    environment:
      S3_ACCESS_KEY: ${STORAGE_ACCESS_KEY:?set STORAGE_ACCESS_KEY in .env}
      S3_SECRET_KEY: ${STORAGE_SECRET_KEY:?set STORAGE_SECRET_KEY in .env}
    ports:
      - "${STORAGE_PORT:-8333}:8333"
    volumes:
      - storage_data:/data
    healthcheck:
      test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8333/"]
      interval: 5s
      timeout: 3s
      retries: 12
    restart: unless-stopped

  app:
    image: docker.io/gotnonamedocker/memoryatlas-selfhost:stable
    ports:
      - "${APP_PORT:-8080}:8080"
    environment:
      App__BaseUrl: ${APP_URL:?set APP_URL in .env}
      Jwt__SecretKey: ${JWT_SECRET:?set JWT_SECRET in .env}
      Setup__Token: ${SETUP_TOKEN:?set SETUP_TOKEN in .env}
      Storage__AccessKey: ${STORAGE_ACCESS_KEY:?set STORAGE_ACCESS_KEY in .env}
      Storage__SecretKey: ${STORAGE_SECRET_KEY:?set STORAGE_SECRET_KEY in .env}
      Storage__PublicEndpoint: ${STORAGE_PUBLIC_ENDPOINT:?set STORAGE_PUBLIC_ENDPOINT in .env}
      ConnectionStrings__DefaultConnection: "Host=postgres;Port=5432;Database=memory_atlas;Username=memory_atlas;Password=${DB_PASSWORD}"
      Storage__Endpoint: ${STORAGE_ENDPOINT:-storage:8333}
      Storage__UseSsl: ${STORAGE_USE_SSL:-false}
      Storage__BucketName: ${STORAGE_BUCKET_NAME:-memory-atlas}
      RateLimit__AuthPermitLimit: ${AUTH_RATE_LIMIT:-20}
    volumes:
      - data_protection_keys:/home/app/.aspnet/DataProtection-Keys
    depends_on:
      postgres:
        condition: service_healthy
      storage:
        condition: service_healthy
    restart: unless-stopped

volumes:
  postgres_data:
  storage_data:
  data_protection_keys:

Why STORAGE_PUBLIC_ENDPOINT matters: the browser talks to object storage directly — presigned uploads go straight there, and finished photos load from it. It must be an address your browser can reach, with the storage port published.

⁠Configuration

VariableRequiredDefaultPurpose
APP_URLyes—The address people use for this instance, e.g. https://atlas.example.com
SETUP_TOKENyes—Creates the first account and resets passwords. No email is sent, so this is the only way in
DB_PASSWORDyes—PostgreSQL password. Read only when the volume is first created
JWT_SECRETyes—Signs sessions. At least 32 characters. Changing it signs everybody out
STORAGE_ACCESS_KEY / STORAGE_SECRET_KEYyes—Object storage credentials
STORAGE_PUBLIC_ENDPOINTyes—Storage address as the browser sees it (localhost:8333, nas.local:8333, …)
APP_PORTno8080Published app port
STORAGE_PORTno8333Published storage port
STORAGE_ENDPOINTnostorage:8333Storage address as the app sees it
STORAGE_USE_SSLnofalseWhether the browser reaches storage over TLS
STORAGE_BUCKET_NAMEnomemory-atlasBucket, created on first use
AUTH_RATE_LIMITno20Sign-in attempts per client per minute
DOMAINno—Only for the optional HTTPS front door

⁠Tags

TagMeaning
stableNewest release. Moves when you pull, and is rebuilt monthly for security patches
0.1.0, 0.1A pinned release. Choose one to decide for yourself when the instance moves
rebuild-YYYYMMDDA dated security rebuild. Same code, newer base image
sha-abc1234The exact commit an image was built from

⁠Volumes and backups

VolumeHolds
postgres_dataPlaces, visits, trips, accounts, metadata
storage_dataThe original photos and videos, plus thumbnails
data_protection_keysKeys that keep sessions valid across restarts

All three matter, and .env matters more: without DB_PASSWORD the database is unreachable, and without STORAGE_SECRET_KEY the stored photos cannot be read. Copy the volumes while stopped.

⁠HTTPS

Plain HTTP on port 8080 works on a trusted network. For a public instance, terminate TLS in front of the app and set APP_URL and STORAGE_PUBLIC_ENDPOINT to the https:// address. Serving storage from the same origin as the app (a reverse proxy forwarding the bucket path) avoids the mixed-content rule entirely.

⁠What this edition does not have

  • No email. Nothing is ever sent: invitations are links you copy from the Members page, and a password is reset at /setup with the setup token. Sign-up is closed; accounts are invitation-only.
  • No billing, no operator panel, no marketing pages. They are not in this image at all, and the build fails if one of them is ever added back.
  • No storage quota and no member cap.

⁠Licence

Free to run for yourself or inside your own organisation, on as many instances as you like. Not for resale, and not for offering to others as a hosted or managed service — that needs a commercial licence. The full terms ship inside the image:

docker run --rm --entrypoint cat gotnonamedocker/memoryatlas-selfhost:stable /app/LICENSE
docker run --rm --entrypoint cat gotnonamedocker/memoryatlas-selfhost:stable /app/THIRD-PARTY-NOTICES

⁠Security

The container runs as a non-root user. Media tools (ffmpeg, heif-convert, exiftool) are invoked with the file protocol only and a five-minute deadline, so a crafted upload cannot reach beyond the file it came in as or hold the import queue. stable is rebuilt monthly so patched packages reach you without waiting for a feature release. Report a vulnerability via memoryatlas.eu⁠.

Tag summary

Content type

Image

Digest

sha256:ff4c1f5dd…

Size

400.1 MB

Last updated

about 9 hours ago

docker pull gotnonamedocker/memoryatlas-selfhost:sha-4d144ae