Memory Atlas på maskinvare du kontrollerer, som tre containere: appen, PostgreSQL og objektlageret. Den samme applikasjonen som den hostede tjenesten, uten betaling, uten operatørpanel og uten markedsføringssider.

Domene, database og lagringskonto er ikke nødvendig, og det er ingenting å sette opp for e-post. HTTPS er det eneste som kan legges til senere.
| stable | Følger den nyeste utgivelsen og flytter seg når du henter. |
| <version> | En fastsatt versjon. Bruk en slik for å bestemme selv når instansen flytter seg. |
| sha-<commit> | Den nøyaktige commiten en image-fil ble bygget fra. |
Begge filene legges i en egen mappe. Compose leser compose.yaml og .env av seg selv, så ingen kommando trenger å nevne dem.
compose.yaml
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: # The app migrates the database as it starts and does not retry: on a first boot it must not # reach Postgres before Postgres accepts connections. 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: # A bare GET on the S3 port answers 200 with no credentials and no signing, which makes it the # cheapest way to tell "serving" from "merely bound". 127.0.0.1 rather than localhost, which # also resolves to ::1 while SeaweedFS listens on IPv4 only. 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:?set DB_PASSWORD in .env}" 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:
.env
DB_PASSWORD= STORAGE_ACCESS_KEY= STORAGE_SECRET_KEY= JWT_SECRET= SETUP_TOKEN= APP_URL=http://localhost:8080 STORAGE_PUBLIC_ENDPOINT=localhost:8333
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
docker compose up -d
Den første starten tar omtrent ett minutt: appen kjører databasemigreringene sine og oppretter lagringsbøtta. Åpne deretter adressen i APP_URL — så lenge ingen første konto finnes, fører hver adresse på instansen til oppsettssiden.
Oppbevar SETUP_TOKEN et annet sted enn på serveren. Det oppretter den første kontoen, og det er den eneste måten å nullstille et passord her — denne utgaven sender ingen e-post, så det finnes ingen lenke å vente på. Hvis det blir borte, har containeren det fortsatt — Passord-avsnittet nedenfor har kommandoen som skriver det ut.
Porter skrives som host:container, så 8080:8080 publiserer port 8080 fra appen på port 8080 på verten. Alt annet er miljøvariabler, lest fra .env ved siden av compose.yaml.
| 8080:8080 | Webgrensesnitt og API. |
| 8333:8333 | Objektlager. Nettleseren laster direkte opp hit, så denne porten må være tilgjengelig fra nettleseren. Legger du til HTTPS nedenfor, serverer Caddy lageret fra appens eget opphav i stedet. |
Bare gruppen «Påkrevd» må stå i .env. Resten sender compose-filen inn med verdien som vises; legg til en linje i .env for å endre den.
Påkrevd
| APP_URL | Adressen folk bruker for å nå denne instansen. Hver invitasjonslenke bygges fra den. |
| SETUP_TOKEN | Gir rett til den første oppsettet, og til å nullstille passord etterpå. Seksten tegn eller mer. |
| DB_PASSWORD | Passordet til den medfølgende PostgreSQL. Brukes bare inne i stakken, og låses til databasen første gang den opprettes. |
| JWT_SECRET | Signerer hver sesjon. 32 tegn eller mer; endrer du den, logges alle ut. |
| STORAGE_ACCESS_KEY STORAGE_SECRET_KEY | Legitimasjon til det medfølgende objektlageret. Den signerer hver opplasting og nedlasting. |
| STORAGE_PUBLIC_ENDPOINT | Adressen nettleseren bruker mot objektlageret — ikke adressen appen bruker. host:port ved vanlig HTTP, eller også protokollen (https://<domain>) når HTTPS er lagt til nedenfor. |
Satt av stakken
| APP_PORT | Host-porten appen publiseres på. Endre den hvis 8080 er opptatt. |
| STORAGE_PORT | Host-porten det medfølgende lageret publiseres på. Endre den hvis 8333 er opptatt. |
| STORAGE_ENDPOINT | Der appen når lageret inne i stakken: storage:8333, lagertjenesten. Pek den et annet sted for å bruke lager du allerede har. |
| STORAGE_USE_SSL | false for det medfølgende lageret, som snakker vanlig HTTP. true for en leverandør som serverer TLS. |
| STORAGE_BUCKET_NAME | Bøtta appen har objektene sine i. Opprettes ved første oppstart. Endrer du den senere, skjules bildene som allerede ligger der. |
| AUTH_RATE_LIMIT | Innloggings- og oppsettforsøk per adresse per minutt, høyere enn appens egen standard fordi første oppsett er flere beskyttede kall på rad av én person. |
Valgfritt
| DOMAIN | Leses bare av HTTPS-tjenesten nedenfor, der den er sertifikatets navn. |
Enhver innstilling i appens egen konfigurasjon kan settes på samme måte, med to understreker for hvert kolon: Upload__MaxVideoSizeMB=2048. Legg den til i environment:-blokken til app-tjenesten.
| postgres_data | Hvert sted, hvert besøk, hver bildepost og hver konto. |
| storage_data | Selve bildene og videoene. |
| data_protection_keys | Nøkler som beskytter dataene. Mister du dem, blir alle logget ut; ingen data går tapt. |
Caddy henter og fornyer sertifikatet selv, og serverer objektlageret fra samme opphav — en nettleser på en https-side laster ikke opp til en http-adresse. Legg denne tjenesten til i compose.yaml, og de to volumene den bruker til listen på slutten av samme fil:
compose.yaml
caddy: image: caddy:2-alpine ports: - "80:80" - "443:443" environment: DOMAIN: ${DOMAIN:?set DOMAIN in .env} STORAGE_BUCKET_NAME: ${STORAGE_BUCKET_NAME:-memory-atlas} volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro - caddy_data:/data - caddy_config:/config depends_on: - app restart: unless-stopped
compose.yaml
volumes: postgres_data: storage_data: data_protection_keys: caddy_data: caddy_config:
Lagre Caddyfile ved siden av compose.yaml, uendret. Den sender bøtte-stien til objektlageret og alt annet til appen, hindrer at tidsgrensene kutter en stor video, og holder instansen unna søkemotorer:
Caddyfile
{$DOMAIN} { handle /{$STORAGE_BUCKET_NAME:memory-atlas}/* { reverse_proxy storage:8333 { transport http { read_timeout 0s write_timeout 0s } } } handle { reverse_proxy app:8080 { transport http { read_timeout 0s write_timeout 0s } } } encode gzip header { X-Forwarded-Proto https X-Forwarded-For {remote_host} X-Robots-Tag "noindex, nofollow" } }
Sett deretter DOMAIN til domenet, og APP_URL og STORAGE_PUBLIC_ENDPOINT til https://<domain> — med protokoll, uten port. DNS for domenet må allerede peke på denne maskinen, og portene 80 og 443 må være tilgjengelige, fordi Caddy spør Let's Encrypt om sertifikatet ved første oppstart. Start stakken igjen:
docker compose up -d
Hemmelighetene i .env og de tre volumene overlever. Databasemigreringer kjøres ved oppstart, så det finnes ikke noe eget migreringstrinn.
docker compose pull && docker compose up -d
Denne utgaven sender ingen e-post, så det finnes ingen tilbakestillingslenke å vente på: et passord endres på /setup, med oppsettstokenet. Det er samme side som lagde den første kontoen, og den virker fortsatt etter at instansen er satt opp.

docker compose exec app printenv Setup__Token
Alle som er innlogget på den kontoen blir logget ut, fordi tilbakestillingen trekker tilbake oppdateringstokenene. Ingenting annet på instansen endres.
Medlemmer kan ikke gjøre dette selv. Innloggingssiden sier det i stedet for å tilby en tilbakestillingslenke, så den som har glemt passordet spør deg.
Stopp stakken først, så kopien ikke tas midt i en skriving.
for v in postgres_data storage_data data_protection_keys; do docker run --rm -v memory-atlas-selfhost_$v:/data -v "$PWD":/backup alpine \ tar czf /backup/$v.tar.gz -C /data . done
I .env ligger nøklene som gjør volumene lesbare — en kopi uten den er ikke en sikkerhetskopi. Slik gjenoppretter du: bygg mappen på nytt, legg inn din egen .env før første start, pakk ut arkivene i de samme volumenavnene, og start.
| Appen starter på nytt et par ganger ved første oppstart | PostgreSQL initialiseres fortsatt og avviser tilkoblinger. Omstartspolicyen henter appen tilbake når den er oppe. Det skjer én gang, på en fersk installasjon. |
| /health melder objectStorage som unhealthy | Lageret er ikke oppe ennå, eller bøtta kunne ikke opprettes. Sjekk legitimasjonen og endepunktene. |
| Bilder lastes opp fra serveren, men ikke fra nettleseren | STORAGE_PUBLIC_ENDPOINT er ikke tilgjengelig fra nettleseren, eller lagerporten er ikke publisert. |
| Alle er logget ut etter en omstart | Volumet data_protection_keys mangler eller er opprettet på nytt. |
| Oppsettssiden sier at tokenet er feil | Verdien er ikke SETUP_TOKEN fra .env. |
| Utgave | Ingen betaling, ingen operatørpanel og ingen markedsføringssider. De finnes ikke i denne image-filen i det hele tatt, og bygget feiler hvis noe av det kommer tilbake. |
| Grenser | Ingen lagringskvote og ingen medlemsgrense: det finnes ingen plan å begrense dem mot. |
| Lisens | Gratis å kjøre for deg selv eller internt i egen organisasjon, på så mange instanser du vil. Ikke gratis å videreselge, og ikke gratis å tilby andre som en driftet tjeneste — det krever en kommersiell lisens. De fullstendige vilkårene følger med image-et som LICENSE. |
| E-post | Ingen. Denne utgaven sender ingenting i det hele tatt: invitasjoner er lenker du kopierer fra medlemssiden og sender selv, og et passord nullstilles fra oppsettssiden med oppsettstokenet. |
| Image | Det selvhostede image-filen publiseres separat fra den hostede, fordi det hostede bygget inneholder betalingsleverandørene og operatørpanelet. |
Memory Atlas
🇪🇺 Bygget og driftet i Europa
© 2026 Memory Atlas
Denne siden finnes også på engelsk.
English