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.

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.
| stable | Segue 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. |
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
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
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.
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.
| 8080:8080 | Interfaccia web e API. |
| 8333:8333 | Storage 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. |
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_URL | L'indirizzo con cui si raggiunge questa istanza. Ogni link di invito viene costruito da lì. |
| SETUP_TOKEN | Autorizza la prima configurazione e, dopo, la reimpostazione delle password. Sedici caratteri o più. |
| DB_PASSWORD | Password del PostgreSQL incluso. Usata solo all'interno dello stack e fissata nel database la prima volta che viene creato. |
| JWT_SECRET | Firma ogni sessione. 32 caratteri o più; cambiarla disconnette tutti. |
| STORAGE_ACCESS_KEY STORAGE_SECRET_KEY | Credenziali dello storage a oggetti incluso. Firma ogni caricamento e ogni scaricamento. |
| STORAGE_PUBLIC_ENDPOINT | L'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_PORT | La porta dell'host su cui l'app viene pubblicata. Cambiala se 8080 è occupata. |
| STORAGE_PORT | La porta dell'host su cui viene pubblicato lo storage incluso. Cambiala se 8333 è occupata. |
| STORAGE_ENDPOINT | Dove 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_SSL | false per lo storage incluso, che parla HTTP semplice. true per un fornitore che serve TLS. |
| STORAGE_BUCKET_NAME | Il bucket in cui l'app tiene i suoi oggetti. Creato al primo avvio. Cambiarlo dopo nasconde le foto già presenti. |
| AUTH_RATE_LIMIT | Tentativi 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
| DOMAIN | Letto 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.
| postgres_data | Ogni luogo, visita, scheda foto e account. |
| storage_data | Le foto e i video stessi. |
| data_protection_keys | Chiavi di protezione dei dati. Perderle disconnette tutti; nessun dato va perso. |
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
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
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.

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.
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.
| L'app si riavvia un paio di volte al primo avvio | PostgreSQL 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 unhealthy | Lo 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 browser | STORAGE_PUBLIC_ENDPOINT non è raggiungibile dal browser, oppure la porta dello storage non è pubblicata. |
| Dopo un riavvio tutti sono disconnessi | Manca il volume data_protection_keys o è stato ricreato. |
| La pagina di configurazione dice che il token è sbagliato | Il valore non è il SETUP_TOKEN nel .env. |
| Edizione | Nessuna fatturazione, nessun pannello operatore e nessuna pagina di marketing. Non sono affatto in questa immagine, e la compilazione fallisce se uno di essi ritorna. |
| Limiti | Nessuna quota di spazio e nessun tetto ai membri: non c'è nessun piano che li limiti. |
| Licenza | Gratis 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. |
| Posta | Nessuna. 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. |
| Immagine | L'immagine self-hosted viene pubblicata separatamente da quella ospitata, perché la build ospitata include i fornitori di pagamento e il pannello operatore. |
Memory Atlas
🇪🇺 Ideato e ospitato in Europa
© 2026 Memory Atlas
Questa pagina è disponibile anche in inglese.
English