Appwrite self-hosted con Docker: instalación, permisos y una app en tiempo real
Al reproducir se carga YouTube (Google), que puede guardar datos en tu navegador y registrar la reproducción. PrivacidadVer en YouTube (se abre en pestaña nueva)
Instalo Appwrite con un comando de Docker y armo un muro en tiempo real con login, fotos y Sites. Con los permisos bien explicados y los errores comunes.
En resumen: Appwrite es un backend open source que puedes instalar tú mismo: login, base de datos, archivos, tiempo real, funciones y hosting en un mismo lugar. Yo lo veo como una alternativa open source a Firebase. En esta guía lo instalo self-hosted con un comando de Docker en mi computadora, preparo el backend desde la consola y conecto una app de ejemplo: un muro de mensajes con React y Vite donde dos usuarios publican texto y fotos que aparecen al instante, y que al final publico con Appwrite Sites. La parte más importante son los permisos: te explico cómo dejarlos bien.
Este artículo acompaña al video de arriba. Si prefieres leer, aquí está todo paso a paso, con los comandos y el código listos para copiar. La app de ejemplo está en el repositorio: kevorojas/appwrite-muro.
Versiones probadas: Appwrite 2.4.0 self-hosted, SDK web
appwrite28, React 19 y Vite 8, en macOS con Docker Desktop. La guía oficial de instalación hoy ya muestra la 2.5.0: el proceso debería ser el mismo, pero yo no la probé, y algunos nombres de la consola pueden cambiar.
¿Qué es Appwrite?
Appwrite es una plataforma de backend open source. Te da las piezas que casi toda app necesita y que nadie quiere escribir desde cero:
- Auth: registro, login y sesiones (email y contraseña, y otros métodos).
- Databases: bases de datos con tablas, filas y columnas.
- Storage: buckets para guardar archivos, como fotos.
- Realtime: avisa a tu app al instante cuando algo cambia.
- Functions: código tuyo que corre en el servidor.
- Messaging: emails, SMS y notificaciones push.
- Sites: hosting para el frontend, parecido a Vercel o Netlify.
Su lema oficial es "The open-source cloud for agents & developers": la nube open source para agentes y desarrolladores. La comparación con Firebase es mía, porque cubre muchas de las mismas necesidades. La diferencia que me interesa: Appwrite lo puedes instalar en tu propio servidor o en tu computadora. Si prefieres no administrar nada, también existe Appwrite Cloud.
Trae además un servidor MCP, para que un agente de IA como Claude trabaje con tu proyecto. Eso queda para otra guía.
Qué necesitas antes de empezar
- Docker (en Mac, Docker Desktop) con Docker Compose v2.
- Node.js 20 o superior, para la app de ejemplo.
- Memoria: la documentación pide 2 núcleos, 4 GB de RAM y 2 GB de swap. En mi Mac, Appwrite en reposo usa unos 2 GB de memoria y alrededor del 5 % de CPU.
- Los puertos 80 y 443 libres. Asegúrate de que ninguna otra app los esté usando antes de instalar.
- Un navegador con una ventana de incógnito, para probar con dos usuarios a la vez.
Sistema operativo: todo lo probé en macOS. En Linux debería ser igual; en Windows el comando de instalación cambia (la documentación oficial tiene la versión para PowerShell). No lo probé fuera de Mac.
Instalar Appwrite self-hosted con Docker
Crea una carpeta de trabajo y corre el instalador. Este es el comando que usé, con la versión fijada en 2.4.0:
mkdir -p appwrite-prueba && cd appwrite-prueba
docker run -it --rm \
--publish 127.0.0.1:20080:20080 \
--volume /var/run/docker.sock:/var/run/docker.sock \
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
--entrypoint="install" \
appwrite/appwrite:2.4.0
Qué hace cada parte:
--publish 127.0.0.1:20080:20080: el asistente de instalación queda en tu computadora, en el puerto 20080, y no se expone a tu red./var/run/docker.sock: el instalador necesita hablar con tu Docker para levantar los contenedores."$(pwd)"/appwrite: ahí deja eldocker-compose.ymly el.envde tu instalación. Los vas a necesitar si algún día cambias el puerto.
Si quieres la versión más nueva, cambia 2.4.0 por la que muestre la guía oficial (hoy, 2.5.0).
El asistente web
La terminal imprime una dirección del tipo http://localhost:20080/?secret=.... Ábrela en el navegador. Ese secret es privado: no lo compartas ni lo muestres en pantalla.
Yo dejé casi todo por defecto:
- Hostname:
localhost, sin HTTPS (es una instalación local). - Base de datos: elegí Postgres.
- Configuración avanzada: por defecto (HTTP en el 80, HTTPS en el 443).
- Secret key: guárdala en un lugar seguro. Se muestra una sola vez.
- Cuenta de administrador: tu email y una contraseña.
Al confirmar, el instalador levanta 17 contenedores (en la 2.4.0; en otras versiones el número puede cambiar).
La primera vez tarda. Docker tiene que descargar todas las imágenes: en mi caso fueron unos 20 minutos. En el video tarda segundos porque ya las tenía descargadas.
Qué son los 17 contenedores
No hace falta conocerlos uno por uno. Esta es la idea:
- Traefik es la puerta de entrada: recibe todo lo que llega al puerto 80 y lo reparte.
- La API de Appwrite atiende a tu app y a la consola.
- Postgres guarda los datos y Redis hace de caché y de cola.
- Realtime mantiene los WebSockets que avisan de los cambios.
- Varios workers hacen el trabajo de fondo (emails, borrados, estadísticas, builds).
- El ejecutor corre tus funciones y compila tus sitios.
Para verlos:
docker ps --format "table {{.Names}}\t{{.Status}}"
La consola y el proyecto
Abre http://localhost e inicia sesión con la cuenta del asistente. La instalación trae un proyecto de prueba creado. Yo lo renombré a Muro en Settings; también puedes crear uno nuevo.
En el menú de la izquierda están las secciones que vas a usar: Auth, Databases, Storage y Sites. También hay Functions, Messaging y más.
Copia el Project ID (está en Overview y en Settings). Lo vas a necesitar en el .env de la app.
Preparar el backend del muro
La app de ejemplo espera estos nombres. Si usas otros IDs, cámbialos en el .env.
1. Login con email y contraseña
Auth → Settings: verifica que Email/Password esté activado. Viene activado por defecto.
2. Registrar la plataforma Web
Appwrite solo acepta peticiones de los orígenes que registraste. Si te saltas este paso, el navegador bloquea las llamadas y la app no funciona.
Overview → Add platform → Web:
- Framework: React
- Name:
Muro local - Hostname:
localhost(cubrehttp://localhost:5173, donde corre Vite)
Después el asistente ofrece un prompt para tu agente de IA y los pasos para conectar una app nueva. Para esta app no hace falta: ya está hecha.
3. Base de datos y tabla mensajes
- Databases → Create database. Name:
Muro· Database ID:muro(escríbelo a mano en vez de dejar el automático). - Create table. Name:
Mensajes· Table ID:mensajes. - En Columns, crea estas cuatro. En Type elige Varchar: el tipo por defecto es Text, que no tiene tamaño.
| Columna | Tipo | Tamaño | Obligatoria | Para qué |
|---|---|---|---|---|
texto | Varchar | 500 | Sí | El mensaje |
autorId | Varchar | 36 | Sí | ID del usuario que lo escribió |
autorNombre | Varchar | 128 | Sí | Nombre que se muestra en el muro |
fotoId | Varchar | 36 | No | ID de la foto en el bucket, si hay |
La fecha no hace falta: Appwrite agrega $createdAt a cada fila.
4. Bucket fotos
- Storage → Create bucket. Name:
Fotos· Bucket ID:fotos. - En Settings del bucket:
- Maximum file size:
5MB (la app también lo valida antes de subir). - Allowed file extensions:
jpg,jpeg,png,webp,gif.
- Maximum file size:
Los permisos de la tabla y del bucket van en la sección siguiente, porque son la parte que más fácil se deja mal.
Permisos en Appwrite: cómo dejarlos bien
La regla que hay que entender: los permisos de una tabla aplican a todas sus filas, y los de un bucket, a todos sus archivos. Si le das Delete a todos los usuarios en la tabla, cualquier usuario puede borrar cualquier mensaje, aunque la app esconda el botón.
Para un muro, lo que quieres es esto:
- Cualquier usuario con sesión puede publicar y puede leer todos los mensajes y fotos.
- Solo el autor puede editar o borrar lo suyo.
La forma de lograrlo: la tabla y el bucket solo dejan crear, y cada fila y cada archivo llevan sus propios permisos.
La tabla mensajes
En la pestaña Settings (o Security) de la tabla:
- Add role → All users y marca solo Create. Guarda con el botón Update de esa sección.
- Activa Row level security y guarda con su botón Update.
- Revisa que los dos botones Update queden en gris. Blanco quiere decir que hay cambios sin guardar.
El bucket fotos
Lo mismo, en Settings del bucket:
- All users con solo Create → Update.
- File level security activado → Update.
- Los dos botones en gris. Si dudas, refresca la página y revisa que el interruptor siga encendido.
No agregues Read ni Delete al bucket. El texto de la consola sugiere que con File security hacen falta permisos del bucket y del archivo. Siguiendo eso, en una prueba le di Read y Delete al bucket, pero el interruptor de File security no había quedado guardado. Resultado: cualquier usuario podía borrar fotos ajenas, y borré una foto real de prueba con otro usuario. Con solo Create en el bucket y File security guardado, los permisos de cada archivo alcanzan.
Los permisos de cada fila y cada archivo los pone la app
Con Row level security y File level security activados, Appwrite deja hacer algo si lo permite la tabla (o el bucket) o la fila (o el archivo). La app crea cada mensaje y cada foto con estos permisos, en src/lib/appwrite.js:
const permisos = [
Permission.read(Role.users()), // cualquier usuario con sesión lo lee
Permission.update(Role.user(usuario.$id)), // solo el autor lo edita
Permission.delete(Role.user(usuario.$id)), // solo el autor lo borra
]
¿Y Read en la tabla? No hace falta, porque cada fila ya trae read(users). Si lo agregas, este muro sigue funcionando igual, pero la lectura deja de depender de la fila: el día que quieras mensajes privados, ese permiso de la tabla los haría visibles para todos.
Un matiz sobre "All users": según la documentación de Appwrite, Role.users() incluye también las sesiones anónimas. Esta app no las usa. Si tampoco las usas tú, revisa que el método Anonymous esté apagado en Auth → Settings.
Pruébalo con la API, no con la app
La app esconde el botón de borrar en los mensajes ajenos, pero eso es comodidad: la seguridad la ponen los permisos. Para comprobarlo, crea un tercer usuario desde la terminal e intenta borrar un mensaje de otro. Reemplaza PROJECT_ID y ID_DEL_MENSAJE (lo copias de la tabla en la consola):
P=PROJECT_ID
curl -s -c /tmp/intruso -H "X-Appwrite-Project: $P" -H "Content-Type: application/json" \
-X POST http://localhost/v1/account \
-d '{"userId":"unique()","email":"intruso@prueba.com","password":"intruso-2026","name":"Intruso"}' > /dev/null
curl -s -b /tmp/intruso -c /tmp/intruso -H "X-Appwrite-Project: $P" -H "Content-Type: application/json" \
-X POST http://localhost/v1/account/sessions/email \
-d '{"email":"intruso@prueba.com","password":"intruso-2026"}' > /dev/null
curl -s -b /tmp/intruso -H "X-Appwrite-Project: $P" \
-X DELETE http://localhost/v1/tablesdb/muro/tables/mensajes/rows/ID_DEL_MENSAJE
Con los permisos bien puestos, la respuesta es un 401: "The current user is not authorized to perform the requested action.". Así lo probé antes de grabar: todos los usuarios con sesión ven mensajes y fotos, sin sesión no se ve nada, nadie borra lo ajeno y el autor sí borra lo suyo.
Si seguiste el video
En la grabación los permisos de la tabla y del bucket quedaron distintos. Los correctos son estos:
- Tabla
mensajes: All users solo con Create, y Row level security activado. - Bucket
fotos: All users solo con Create, y File level security activado.
Cada mensaje y cada foto traen sus propios permisos, que pone la app al crearlos: lectura para los usuarios con sesión, y edición y borrado solo para el autor.
Conectar la app
Clona el repo y crea el .env:
git clone [kevorojas/appwrite-muro](https://github.com/kevorojas/appwrite-muro)
cd appwrite-muro
cp .env.example .env
Completa el .env con el Project ID que copiaste:
VITE_APPWRITE_ENDPOINT=http://localhost/v1
VITE_APPWRITE_PROJECT_ID=<tu-project-id>
VITE_APPWRITE_DATABASE_ID=muro
VITE_APPWRITE_TABLE_ID=mensajes
VITE_APPWRITE_BUCKET_ID=fotos
Estos IDs no son secretos: terminan en el JavaScript que descarga el navegador. Lo que protege los datos son los permisos. El .env igual queda fuera de Git.
npm install
npm run dev
La app queda en http://localhost:5173.
Las líneas que hablan con Appwrite
Todo lo que toca Appwrite está en src/lib/appwrite.js. Los componentes de React solo llaman a sus funciones. Lo esencial:
El cliente, que sabe a qué servidor y a qué proyecto hablarle:
const client = new Client()
client.setEndpoint(ENDPOINT).setProject(PROJECT_ID)
const account = new Account(client) // usuarios y sesiones
const tablesDB = new TablesDB(client) // tablas y filas
const storage = new Storage(client) // archivos
const realtime = new Realtime(client) // eventos en vivo
Publicar: primero sube la foto (si hay) y después crea la fila, las dos con los permisos de la sección anterior:
const archivo = await storage.createFile({
bucketId: BUCKET_ID,
fileId: ID.unique(),
file: foto,
permissions: permisos,
})
await tablesDB.createRow({
databaseId: DATABASE_ID,
tableId: TABLE_ID,
rowId: ID.unique(),
data: { texto, autorId: usuario.$id, autorNombre: usuario.name, fotoId: archivo.$id },
permissions: permisos,
})
Tiempo real: la app se suscribe al canal de las filas de la tabla y reacciona cuando alguien publica o borra:
const canalMensajes = Channel.tablesdb(DATABASE_ID).table(TABLE_ID).row()
realtime.subscribe(canalMensajes, (evento) => {
const fila = evento.payload
if (evento.events.some((e) => e.endsWith('.create'))) alCrear(fila)
if (evento.events.some((e) => e.endsWith('.delete'))) alBorrar(fila)
})
Appwrite solo manda eventos de las filas que el usuario tiene permiso de leer. Otra razón para cuidar los permisos.
Desde Appwrite 1.8 la base de datos usa la API TablesDB: tablas, filas y columnas. Antes eran colecciones, documentos y atributos. Si encuentras tutoriales con
databases.createDocument, son de antes del cambio.
Probarlo con dos usuarios
- Abre
http://localhost:5173y crea un usuario. - Abre una ventana de incógnito (u otro navegador), crea un segundo usuario y pon las dos ventanas lado a lado.
- Publica desde una: el mensaje aparece al instante en la otra, sin recargar.
- Publica con una foto.
- Fíjate que el botón Borrar solo aparece en tus propios mensajes. Si borras uno, desaparece en las dos ventanas.
En la consola puedes ver lo que pasó: los dos usuarios en Auth → Users, los mensajes en Databases → muro → mensajes y las fotos en Storage → fotos. El backend de autenticación, la base de datos y los archivos ya estaban hechos: solo los conectaste.
Publicar la app con Appwrite Sites
Appwrite también aloja el frontend. Lo normal es conectar un repositorio de GitHub, pero GitHub no puede llegar a un Appwrite que corre en tu computadora. Así que la subo compilada:
-
Compila y empaqueta. El script toma los valores de tu
.env, porque Vite los mete en el JavaScript al compilar:npm run empaquetar # npm run build + tar -czf sitio.tar.gz -C dist . -
Sites → Create site y elige la subida manual (no conectes GitHub):
- Site name:
Muro - Framework:
Other - Install command y Build command: vacíos (la app ya va compilada)
- Output directory:
./ - Fallback file:
index.html - Environment variables: ninguna
- Site name:
-
Sube
sitio.tar.gzy haz el deploy. -
Registra el dominio del sitio como otra plataforma Web (Overview → Add platform → Web), con hostname
muro.sites.localhost. No comprobé si Appwrite ya acepta su propio dominio de Sites sin este paso; agregarlo no hace daño. -
Abre http://muro.sites.localhost. Chrome y Firefox resuelven
*.localhostsolos.
En la página del sitio también puedes agregar un dominio propio, ver los logs de las peticiones y cambiar la configuración del build.
Problemas comunes
Los errores más comunes y cómo resolverlos:
| Síntoma | Causa | Solución |
|---|---|---|
La consola no abre en http://localhost, o abre otra cosa | Otra app está usando el puerto 80 o el 443 | Asegúrate de que esos puertos estén libres antes de instalar Appwrite |
| La app dice "Falta registrar la plataforma web" o "No hay conexión con Appwrite"; en la consola del navegador, un error de CORS | No registraste la plataforma Web con el hostname de la app | Overview → Add platform → Web con localhost (y después con el dominio de Sites) |
| La instalación se queda mucho tiempo en "Starting Docker containers" | La primera vez descarga todas las imágenes | Esperar: en mi caso, unos 20 minutos |
| Un permiso o un interruptor "vuelve" a como estaba | Cada sección de Security tiene su propio botón Update | Guardar cada sección y revisar que el botón quede en gris |
| Las fotos se suben pero no se ven | File level security apagado y el bucket sin Read | Encender File security y guardarlo (ver la sección de permisos) |
Los puertos de Appwrite
Appwrite usa los puertos 80 y 443. Antes de instalarlo, asegúrate de que estén libres: si otra app los está usando, la consola no abre o abre otra cosa.
El botón Update que no guarda
Cada sección de Security (los permisos y el interruptor de Row o File security) tiene su propio botón Update. La única señal es el color: blanco quiere decir que hay cambios sin guardar; gris, que se guardaron. En el video se ve el caso del bucket: el interruptor de File security no quedó guardado a la primera. Después de configurar los permisos, refresca la página y revisa que todo siga como lo dejaste.
Apagar o borrar la instalación
Para apagar Appwrite sin perder nada:
cd appwrite-prueba/appwrite
docker compose stop
Para borrarlo todo (cuenta, proyectos, datos y archivos):
cd appwrite-prueba/appwrite
docker compose down -v
Ojo: el -v borra los volúmenes, y con ellos todos tus datos. Las imágenes de Docker quedan, así que una reinstalación no vuelve a tardar 20 minutos. Si no lo vas a usar más, bórralas desde Docker Desktop o con docker image rm.
Preguntas frecuentes
¿Cuánta memoria necesita Appwrite? La documentación pide 2 núcleos, 4 GB de RAM y 2 GB de swap. En mi Mac, en reposo, usa unos 2 GB de memoria y alrededor del 5 % de CPU.
¿Appwrite self-hosted sirve para producción? Se puede, pero esta guía es para una instalación local. Para producción hace falta, como mínimo: un dominio con HTTPS, copias de seguridad de los datos y los archivos, un plan para actualizar versiones y no dejar expuestos el instalador ni la consola. Yo no tengo Appwrite en producción.
¿Self-hosted o Appwrite Cloud?
Self-hosted si quieres aprender, probar en local o tener tus datos en tu servidor, y estás dispuesto a mantenerlo. Cloud si quieres usarlo sin administrar servidores. El código de la app es el mismo: solo cambia el endpoint del .env.
¿Necesito saber React para seguir la guía?
No. La app ya está hecha: solo completas el .env. Lo importante es la configuración de Appwrite, y eso es igual para cualquier framework.
Próximos pasos
Con esto tienes un backend completo corriendo en tu computadora: login, base de datos, archivos, tiempo real y hosting. Lo que me queda por explorar es Functions, Messaging y el servidor MCP, para que un agente de IA trabaje directamente con el proyecto.
Si te interesa la idea de practicar en tu computadora antes de ir a la nube, en esta guía uso Floci para emular Azure con Terraform, también con Docker.
El código de la app está en kevorojas/appwrite-muro. Y si te gustaría ver a Appwrite publicado en un servidor de verdad, déjalo en los comentarios del video.
Siguiente paso
Lo que te recomiendo leer después de este.

Emulador de Azure local: cómo usar Floci con Terraform (lo que funciona y lo que no)
Floci emula Azure en tu computadora. Lo uso con Terraform: un PostgreSQL real, una VM que no existe y AKS con k3s. Sin cuenta de Azure ni tarjeta.
Si te sirvió, ayúdame a llegar a 500 suscriptores en YouTube
Subo tutoriales de herramientas DevOps que voy probando.
Ver el video de este tutorial en YouTube(se abre en pestaña nueva)¿Prefieres el correo?
Recibe el próximo tutorial en tu bandeja cuando salga.
