Saltar al contenido
Kevo Rojas

Appwrite self-hosted con Docker: instalación, permisos y una app en tiempo real

KR
Kevo Rojas
15 oct 2026 · 17 min de lectura
Video del tutorial

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 appwrite 28, 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 el docker-compose.yml y el .env de 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:

  1. Hostname: localhost, sin HTTPS (es una instalación local).
  2. Base de datos: elegí Postgres.
  3. Configuración avanzada: por defecto (HTTP en el 80, HTTPS en el 443).
  4. Secret key: guárdala en un lugar seguro. Se muestra una sola vez.
  5. 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 (cubre http://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

  1. Databases → Create database. Name: Muro · Database ID: muro (escríbelo a mano en vez de dejar el automático).
  2. Create table. Name: Mensajes · Table ID: mensajes.
  3. En Columns, crea estas cuatro. En Type elige Varchar: el tipo por defecto es Text, que no tiene tamaño.
ColumnaTipoTamañoObligatoriaPara qué
textoVarchar500SíEl mensaje
autorIdVarchar36SíID del usuario que lo escribió
autorNombreVarchar128SíNombre que se muestra en el muro
fotoIdVarchar36NoID de la foto en el bucket, si hay

La fecha no hace falta: Appwrite agrega $createdAt a cada fila.

4. Bucket fotos

  1. Storage → Create bucket. Name: Fotos · Bucket ID: fotos.
  2. En Settings del bucket:
    • Maximum file size: 5 MB (la app también lo valida antes de subir).
    • Allowed file extensions: jpg, jpeg, png, webp, gif.

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:

  1. Add role → All users y marca solo Create. Guarda con el botón Update de esa sección.
  2. Activa Row level security y guarda con su botón Update.
  3. 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:

  1. All users con solo Create → Update.
  2. File level security activado → Update.
  3. 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

  1. Abre http://localhost:5173 y crea un usuario.
  2. Abre una ventana de incógnito (u otro navegador), crea un segundo usuario y pon las dos ventanas lado a lado.
  3. Publica desde una: el mensaje aparece al instante en la otra, sin recargar.
  4. Publica con una foto.
  5. 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:

  1. 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 .
    
  2. 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
  3. Sube sitio.tar.gz y haz el deploy.

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

  5. Abre http://muro.sites.localhost. Chrome y Firefox resuelven *.localhost solos.

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íntomaCausaSolución
La consola no abre en http://localhost, o abre otra cosaOtra app está usando el puerto 80 o el 443Asegú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 CORSNo registraste la plataforma Web con el hostname de la appOverview → 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ágenesEsperar: en mi caso, unos 20 minutos
Un permiso o un interruptor "vuelve" a como estabaCada sección de Security tiene su propio botón UpdateGuardar cada sección y revisar que el botón quede en gris
Las fotos se suben pero no se venFile level security apagado y el bucket sin ReadEncender 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.

Azure

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.

23 min de lectura
Sígueme

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.

Uso tu email solo para el newsletter. Política de privacidad