Memory Atlas, selvhostet

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.

Oppsettssiden til en ny instans: felt for oppsettstoken, deretter navn, e-post og passord.

Det du trenger

  • Docker med Compose v2 — underkommandoen docker compose, ikke den gamle docker-compose. Hele stakken kjører i containere.
  • En 64-biters maskin — linux/amd64 eller linux/arm64. Begge publiseres: en x86-64-server, eller et arm64-kort (Raspberry Pi 4 eller 5 med 64-biters OS, en ARM-NAS, Apple silicon). 32-biters ARM publiseres ikke.
  • En maskin som står på, for den er serveren. En PC du har til overs, et NAS eller en liten VPS holder.
  • Rundt 3 GB ledig plass til å begynne med. App-image-filen er 1,7 GB; resten fyller bilder og videoer.

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.

Versjonstagger

stableFø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.

Bruk

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

Lag hemmelighetene

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

Start den

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.

Parametere

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.

Porter

8080:8080Webgrensesnitt og API.
8333:8333Objektlager. 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.

Miljøvariabler

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_URLAdressen folk bruker for å nå denne instansen. Hver invitasjonslenke bygges fra den.
SETUP_TOKENGir rett til den første oppsettet, og til å nullstille passord etterpå. Seksten tegn eller mer.
DB_PASSWORDPassordet til den medfølgende PostgreSQL. Brukes bare inne i stakken, og låses til databasen første gang den opprettes.
JWT_SECRETSignerer hver sesjon. 32 tegn eller mer; endrer du den, logges alle ut.
STORAGE_ACCESS_KEY STORAGE_SECRET_KEYLegitimasjon til det medfølgende objektlageret. Den signerer hver opplasting og nedlasting.
STORAGE_PUBLIC_ENDPOINTAdressen 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_PORTHost-porten appen publiseres på. Endre den hvis 8080 er opptatt.
STORAGE_PORTHost-porten det medfølgende lageret publiseres på. Endre den hvis 8333 er opptatt.
STORAGE_ENDPOINTDer appen når lageret inne i stakken: storage:8333, lagertjenesten. Pek den et annet sted for å bruke lager du allerede har.
STORAGE_USE_SSLfalse for det medfølgende lageret, som snakker vanlig HTTP. true for en leverandør som serverer TLS.
STORAGE_BUCKET_NAMEBøtta appen har objektene sine i. Opprettes ved første oppstart. Endrer du den senere, skjules bildene som allerede ligger der.
AUTH_RATE_LIMITInnloggings- 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

DOMAINLeses 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.

Volumer

postgres_dataHvert sted, hvert besøk, hver bildepost og hver konto.
storage_dataSelve bildene og videoene.
data_protection_keysNøkler som beskytter dataene. Mister du dem, blir alle logget ut; ingen data går tapt.

Valgfritt: HTTPS med domene

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

Oppdatere

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

Passord

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.

Oppsettssiden til en instans som allerede er satt opp: feltet for oppsettstoken, deretter kontoens e-post, et nytt passord og knappen Change password.
  1. Åpne oppsettssiden — APP_URL/setup, adressen folk bruker til denne instansen.
  2. Lim inn oppsettstokenet: SETUP_TOKEN i stakkens env-fil, eller verdien den kjørende containeren skriver ut med kommandoen nedenfor.
  3. Skriv inn kontoens e-post og et nytt passord, og velg Change password. Det er ingen bekreftelse og ingenting sendes — det nye passordet virker ved neste innlogging.
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.

Sikkerhetskopier

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.

Feilsøking

Appen starter på nytt et par ganger ved første oppstartPostgreSQL 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 unhealthyLageret er ikke oppe ennå, eller bøtta kunne ikke opprettes. Sjekk legitimasjonen og endepunktene.
Bilder lastes opp fra serveren, men ikke fra nettleserenSTORAGE_PUBLIC_ENDPOINT er ikke tilgjengelig fra nettleseren, eller lagerporten er ikke publisert.
Alle er logget ut etter en omstartVolumet data_protection_keys mangler eller er opprettet på nytt.
Oppsettssiden sier at tokenet er feilVerdien er ikke SETUP_TOKEN fra .env.

Merknader

UtgaveIngen 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.
GrenserIngen lagringskvote og ingen medlemsgrense: det finnes ingen plan å begrense dem mot.
LisensGratis å 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-postIngen. 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.
ImageDet selvhostede image-filen publiseres separat fra den hostede, fordi det hostede bygget inneholder betalingsleverandørene og operatørpanelet.
Memory Atlas

Memory Atlas

🇪🇺 Bygget og driftet i Europa

© 2026 Memory Atlas

Denne siden finnes også på engelsk.

English