Memory Atlas, self-hosted

Memory Atlas su hardware che controlli, come tre container: l'app, PostgreSQL e lo storage a oggetti. La stessa applicazione del servizio ospitato, senza fatturazione, senza pannello operatore e senza pagine di marketing.

La pagina di configurazione di una nuova istanza: un campo per il token di configurazione, poi nome, email e password.

Cosa ti serve

  • Docker con Compose v2 — il sottocomando docker compose, non il vecchio docker-compose. Tutto lo stack gira in container.
  • Una macchina a 64 bit — linux/amd64 o linux/arm64. Sono pubblicate entrambe: un server x86-64, oppure una scheda arm64 (Raspberry Pi 4 o 5 con sistema a 64 bit, un NAS ARM, Apple silicon). L'ARM a 32 bit non è pubblicato.
  • Una macchina sempre accesa, perché è lei il server. Va bene un computer in più, un NAS o un piccolo VPS.
  • Circa 3 GB di disco per iniziare. L'immagine dell'app pesa 1,7 GB; il resto lo riempiono foto e video.

Non servono dominio, database né account di storage, e non c'è nulla da configurare per la posta. HTTPS è l'unica cosa che si può aggiungere dopo.

Tag di versione

stableSegue l'ultima versione pubblicata e avanza quando fai pull.
<version>Una versione pubblicata, fissata. Usala per decidere tu quando questa istanza si aggiorna.
sha-<commit>Il commit esatto da cui è stata costruita un'immagine.

Uso

Entrambi i file vanno in una cartella loro. Compose legge compose.yaml e .env dal nome, quindi nessun comando deve nominarli.

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

Genera i segreti

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

Avvia

docker compose up -d

Il primo avvio richiede circa un minuto: l'app applica le sue migrazioni del database e crea il suo bucket di storage. Poi apri l'indirizzo in APP_URL — finché non esiste un primo account, ogni indirizzo dell'istanza porta alla pagina di configurazione.

Conserva SETUP_TOKEN fuori dal server. È ciò che crea il primo account e l'unico modo per reimpostare una password qui — questa edizione non manda posta, quindi non c'è nessun link da aspettare. Se va perso, il container ce l'ha ancora: il comando è nella sezione «Password», più sotto.

Parametri

Le porte si scrivono host:container: 8080:8080 pubblica la porta 8080 dell'app sulla porta 8080 dell'host. Tutto il resto sono variabili d'ambiente, lette dal .env accanto a compose.yaml.

Porte

8080:8080Interfaccia web e API.
8333:8333Storage a oggetti. Il browser carica direttamente qui, quindi questa porta deve essere raggiungibile dal browser. Aggiungendo HTTPS più sotto, è Caddy a servire lo storage dalla stessa origine dell'app.

Variabili d'ambiente

Solo il gruppo «Obbligatorie» deve stare nel .env. Il file compose passa già tutto il resto con il valore indicato; aggiungi una riga nel .env per cambiarlo.

Obbligatorie

APP_URLL'indirizzo con cui si raggiunge questa istanza. Ogni link di invito viene costruito da lì.
SETUP_TOKENAutorizza la prima configurazione e, dopo, la reimpostazione delle password. Sedici caratteri o più.
DB_PASSWORDPassword del PostgreSQL incluso. Usata solo all'interno dello stack e fissata nel database la prima volta che viene creato.
JWT_SECRETFirma ogni sessione. 32 caratteri o più; cambiarla disconnette tutti.
STORAGE_ACCESS_KEY STORAGE_SECRET_KEYCredenziali dello storage a oggetti incluso. Firma ogni caricamento e ogni scaricamento.
STORAGE_PUBLIC_ENDPOINTL'indirizzo che il browser usa per lo storage a oggetti — non quello che usa l'app. host:port con HTTP semplice, oppure anche lo schema (https://<domain>) quando aggiungi HTTPS più sotto.

Impostate dallo stack

APP_PORTLa porta dell'host su cui l'app viene pubblicata. Cambiala se 8080 è occupata.
STORAGE_PORTLa porta dell'host su cui viene pubblicato lo storage incluso. Cambiala se 8333 è occupata.
STORAGE_ENDPOINTDove l'app raggiunge lo storage dentro lo stack: storage:8333, il servizio di storage. Puntalo altrove per usare uno storage che hai già.
STORAGE_USE_SSLfalse per lo storage incluso, che parla HTTP semplice. true per un fornitore che serve TLS.
STORAGE_BUCKET_NAMEIl bucket in cui l'app tiene i suoi oggetti. Creato al primo avvio. Cambiarlo dopo nasconde le foto già presenti.
AUTH_RATE_LIMITTentativi di accesso e di configurazione per indirizzo al minuto, sopra il valore predefinito dell'app perché la prima configurazione è una serie di chiamate protette fatte da una sola persona.

Facoltative

DOMAINLetto solo dal servizio HTTPS più sotto, dove è il nome del certificato.

Qualsiasi impostazione della configurazione dell'app si può dare allo stesso modo, con due underscore al posto di ogni due punti: Upload__MaxVideoSizeMB=2048. Aggiungila al blocco environment: del servizio app.

Volumi

postgres_dataOgni luogo, visita, scheda foto e account.
storage_dataLe foto e i video stessi.
data_protection_keysChiavi di protezione dei dati. Perderle disconnette tutti; nessun dato va perso.

Facoltativo: HTTPS con un dominio

Caddy ottiene e rinnova il certificato da sé, e serve lo storage a oggetti dalla stessa origine — un browser su una pagina https non carica su un indirizzo http. Aggiungi questo servizio a compose.yaml, e i due volumi che usa all'elenco in fondo allo stesso file:

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:

Salva il Caddyfile accanto a compose.yaml, senza modifiche. Manda il percorso del bucket allo storage e tutto il resto all'app, evita che i timeout troncino un video grande e tiene l'istanza fuori dai motori di ricerca:

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"
    }
}

Poi imposta DOMAIN al dominio, e APP_URL e STORAGE_PUBLIC_ENDPOINT a https://<domain> — con lo schema, senza la porta. Il DNS del dominio deve già puntare a questa macchina e le porte 80 e 443 devono essere raggiungibili, perché Caddy chiede il certificato a Let's Encrypt al primo avvio. Avvia di nuovo lo stack:

docker compose up -d

Aggiornare

I segreti nel .env e i tre volumi sopravvivono. Le migrazioni del database vengono eseguite all'avvio, quindi non c'è un passo di migrazione.

docker compose pull && docker compose up -d

Password

Questa edizione non invia email, quindi non c'è nessun link di reimpostazione da aspettare: la password si cambia su /setup, con il token di configurazione. È la stessa pagina che ha creato il primo account e continua a funzionare dopo la configurazione dell'istanza.

La pagina di configurazione di un'istanza già configurata: il campo del token di configurazione, poi l'email dell'account, una nuova password e il pulsante Change password.
  1. Apri la pagina di configurazione: APP_URL/setup, l'indirizzo usato per questa istanza.
  2. Incolla il token di configurazione: SETUP_TOKEN nel file env dello stack, o il valore che il container in esecuzione stampa con il comando qui sotto.
  3. Inserisci l'email dell'account e una nuova password, poi scegli Change password. Non c'è un passaggio di conferma e non viene inviato nulla: la nuova password vale al prossimo accesso.
docker compose exec app printenv Setup__Token

Tutte le sessioni di quell'account vengono disconnesse, perché la reimpostazione revoca i refresh token. Nient'altro cambia nell'istanza.

I membri non possono farlo da soli. La pagina di accesso lo dice invece di offrire un link di reimpostazione, quindi chi ha dimenticato la password la chiede a te.

Backup

Ferma prima lo stack, così la copia non viene presa a metà di una scrittura.

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

Il .env contiene le chiavi che rendono leggibili i volumi, quindi una copia senza non è un backup. Per ripristinare: ricrea la cartella, metti il tuo .env prima del primo avvio, estrai gli archivi negli stessi nomi di volume e avviala.

Risoluzione dei problemi

L'app si riavvia un paio di volte al primo avvioPostgreSQL si sta ancora inizializzando e rifiuta le connessioni. Il criterio di riavvio riporta su l'app quando è pronto. Succede una volta sola, a installazione nuova.
/health segnala objectStorage come unhealthyLo storage non è ancora partito, oppure il suo bucket non è stato creato. Controlla le credenziali e gli endpoint.
Le foto si caricano dal server ma non dal browserSTORAGE_PUBLIC_ENDPOINT non è raggiungibile dal browser, oppure la porta dello storage non è pubblicata.
Dopo un riavvio tutti sono disconnessiManca il volume data_protection_keys o è stato ricreato.
La pagina di configurazione dice che il token è sbagliatoIl valore non è il SETUP_TOKEN nel .env.

Note

EdizioneNessuna fatturazione, nessun pannello operatore e nessuna pagina di marketing. Non sono affatto in questa immagine, e la compilazione fallisce se uno di essi ritorna.
LimitiNessuna quota di spazio e nessun tetto ai membri: non c'è nessun piano che li limiti.
LicenzaGratis per uso personale o all'interno della propria organizzazione, su quante istanze vuoi. Non è gratis rivenderlo né offrirlo ad altri come servizio ospitato: serve una licenza commerciale. I termini completi accompagnano l'immagine come LICENSE.
PostaNessuna. Questa edizione non manda nulla: gli inviti sono link che copi dalla pagina dei membri e invii tu, e una password si reimposta dalla pagina di configurazione con il token di configurazione.
ImmagineL'immagine self-hosted viene pubblicata separatamente da quella ospitata, perché la build ospitata include i fornitori di pagamento e il pannello operatore.
Memory Atlas

Memory Atlas

🇪🇺 Ideato e ospitato in Europa

© 2026 Memory Atlas

Questa pagina è disponibile anche in inglese.

English