Your travel photos, mapped and organized on your own server. Self-hosted Memory Atlas.
89
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.
linux/amd64 and linux/arm64 in one manifest list.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_ENDPOINTmatters: 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.
| Variable | Required | Default | Purpose |
|---|---|---|---|
APP_URL | yes | — | The address people use for this instance, e.g. https://atlas.example.com |
SETUP_TOKEN | yes | — | Creates the first account and resets passwords. No email is sent, so this is the only way in |
DB_PASSWORD | yes | — | PostgreSQL password. Read only when the volume is first created |
JWT_SECRET | yes | — | Signs sessions. At least 32 characters. Changing it signs everybody out |
STORAGE_ACCESS_KEY / STORAGE_SECRET_KEY | yes | — | Object storage credentials |
STORAGE_PUBLIC_ENDPOINT | yes | — | Storage address as the browser sees it (localhost:8333, nas.local:8333, …) |
APP_PORT | no | 8080 | Published app port |
STORAGE_PORT | no | 8333 | Published storage port |
STORAGE_ENDPOINT | no | storage:8333 | Storage address as the app sees it |
STORAGE_USE_SSL | no | false | Whether the browser reaches storage over TLS |
STORAGE_BUCKET_NAME | no | memory-atlas | Bucket, created on first use |
AUTH_RATE_LIMIT | no | 20 | Sign-in attempts per client per minute |
DOMAIN | no | — | Only for the optional HTTPS front door |
| Tag | Meaning |
|---|---|
stable | Newest release. Moves when you pull, and is rebuilt monthly for security patches |
0.1.0, 0.1 | A pinned release. Choose one to decide for yourself when the instance moves |
rebuild-YYYYMMDD | A dated security rebuild. Same code, newer base image |
sha-abc1234 | The exact commit an image was built from |
| Volume | Holds |
|---|---|
postgres_data | Places, visits, trips, accounts, metadata |
storage_data | The original photos and videos, plus thumbnails |
data_protection_keys | Keys 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.
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.
/setup with the setup token. Sign-up is closed; accounts are invitation-only.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
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.
Content type
Image
Digest
sha256:ff4c1f5dd…
Size
400.1 MB
Last updated
about 9 hours ago
docker pull gotnonamedocker/memoryatlas-selfhost:sha-4d144ae