Docker setup
Bring up a full Feeblo deployment with the published ghcr.io images plus Postgres, SMTP, and S3-compatible storage wired together in one compose file.
This is the hands-on guide for running all of Feeblo in Docker. The shipped docker-compose.yml only contains the two Feeblo services (server and web); you add the three dependencies (PostgreSQL, SMTP, and S3-compatible media storage) and the environment that ties them together.
This page is based on docker-compose.yml, docker/docker-compose.dev.yml, apps/server/Dockerfile, apps/web/Dockerfile, and .env.example in .reference/.
The two published images
Both images are built from the Dockerfiles in apps/ and pushed to ghcr.io:
| Image | Internal port | Starts with | Notes |
|---|---|---|---|
ghcr.io/g3root/feeblo-server | 8080 | node ./migrate/index.js && exec node ./dist/index.js | Runs migrations automatically on boot, then starts the API |
ghcr.io/g3root/feeblo-web | 4321 | node ./dist/server/entry.mjs | Astro SSR, HOST=0.0.0.0, CLOUDFLARE_ADAPTER=false |
The server image bundles packages/db/src/migrations and the db-migrator build (packages/db-migrator), so you do not run migrations separately — they execute every time the server container starts. If DATABASE_URL is unset the migrator logs "DATABASE_URL not defined, skipping migrations" and exits cleanly, after which the server itself will fail to start (it also needs DATABASE_URL). See the database page for details.
A complete compose file
Drop this in your repo as docker-compose.yml. It merges the shipped server/web services with Postgres, Mailpit (SMTP), and MinIO (S3-compatible), and a one-shot MinIO bucket bootstrap — the same stack the dev compose (docker/docker-compose.dev.yml) uses internally:
services:
server:
container_name: feeblo-server
image: ghcr.io/g3root/feeblo-server:${IMAGE_TAG:-nightly}
restart: always
depends_on:
- postgres
environment:
NODE_ENV: production
PORT: ${SERVER_PORT:-8080}
SERVER_PORT: ${SERVER_PORT:-8080}
APP_URL: ${APP_URL}
API_URL: ${API_URL}
APP_ROOT_DOMAIN: ${APP_ROOT_DOMAIN}
APP_RELEASE: ${IMAGE_TAG:-nightly}
DATABASE_URL: postgres://feeblo:${POSTGRES_PASSWORD}@postgres:5432/feeblo
AUTH_ENCRYPTION_KEY: ${AUTH_ENCRYPTION_KEY}
SMTP_HOST: feeblo-smtp
SMTP_PORT: "1025"
SMTP_FROM_NAME: "Feeblo"
SMTP_FROM_ADDRESS: "noreply@feeblo.com"
MEDIA_UPLOAD_REGION: "us-east-1"
MEDIA_UPLOAD_ENDPOINT: http://feeblo-minio:9002
MEDIA_UPLOAD_ACCESS_KEY_ID: ${MINIO_ROOT_USER}
MEDIA_UPLOAD_SECRET_ACCESS_KEY: ${MINIO_ROOT_PASSWORD}
MEDIA_PUBLIC_BUCKET_NAME: feeblo-media-public
ports:
- "${SERVER_PORT:-8080}:8080"
web:
container_name: feeblo-web
image: ghcr.io/g3root/feeblo-web:${IMAGE_TAG:-nightly}
restart: always
environment:
NODE_ENV: production
HOST: 0.0.0.0
PORT: ${WEB_PORT:-4321}
API_URL: ${API_URL}
APP_URL: ${APP_URL}
APP_ROOT_DOMAIN: ${APP_ROOT_DOMAIN}
APP_RELEASE: ${IMAGE_TAG:-nightly}
ports:
- "${WEB_PORT:-4321}:4321"
depends_on:
- server
postgres:
image: postgres:17-alpine
container_name: feeblo-database
environment:
POSTGRES_USER: feeblo
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: feeblo
volumes:
- postgres:/var/lib/postgresql/data
smtp:
image: axllent/mailpit
container_name: feeblo-smtp
ports:
- "8025:8025" # Mailpit web UI
minio:
image: minio/minio
container_name: feeblo-minio
volumes:
- minio:/data
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
entrypoint: sh
command: -c 'minio server /data --console-address ":9001" --address ":9002"'
mc:
image: minio/mc
container_name: feeblo-mc
depends_on:
- minio
entrypoint: sh
command: |
-c "
sleep 10;
mc alias set myminio http://minio:9002 ${MINIO_ROOT_USER} ${MINIO_ROOT_PASSWORD};
mc mb myminio/feeblo-media-public;
mc anonymous set download myminio/feeblo-media-public;
exit 0;
"
volumes:
postgres:
minio:What the mc container does
The mc service is a one-shot bootstrap: it creates the feeblo-media-public bucket and makes it
anonymously downloadable, which is how Feeblo serves uploaded media over public URLs. It exits
after succeeding; if the bucket already exists mc mb errors harmlessly and the anonymous policy
is re-applied.
The matching .env
# Image tag to deploy (defaults to nightly if omitted)
IMAGE_TAG=nightly
# Public URLs the apps advertise
APP_URL=https://feeblo.example.com
API_URL=https://api.feeblo.example.com
APP_ROOT_DOMAIN=feeblo.example.com
# Ports on the host (optional)
SERVER_PORT=8080
WEB_PORT=4321
# Postgres
POSTGRES_PASSWORD=change-me
# Object storage (MinIO here)
MINIO_ROOT_USER=feeblo
MINIO_ROOT_PASSWORD=change-me-too
# Auth — keep this stable across restarts
AUTH_ENCRYPTION_KEY=generate-a-long-random-stringPut reverse proxies in front
The compose above does not expose Postgres, MinIO, or Mailpit to the host by default — only
server (8080) and web (4321) are published. Put TLS-terminating reverse proxies (or a
Cloudflare tunnel) in front of web and server, and set APP_URL/API_URL to those public
origins. APP_URL and API_URL must be URLs (https://…); APP_ROOT_DOMAIN must be the bare
host (feeblo.example.com) — see the overview for the CORS origin
logic.
Bring it up
docker compose up -d
docker compose logs -f server # watch migrations run, then the API startSanity checks once it's up:
curl https://api.feeblo.example.com/health→OKhttps://api.feeblo.example.com/docs→ Scalar/OpenAPI UIhttps://feeblo.example.com→ the dashboard
Pinning a version
IMAGE_TAG defaults to nightly everywhere (${IMAGE_TAG:-nightly} in the compose, APP_RELEASE reflects it). For production, pin a real tag:
IMAGE_TAG=v1.2.3
docker compose up -dThe server and web images are built from apps/server/Dockerfile and apps/web/Dockerfile using turbo prune to isolate each app's workspace graph — so each image only contains what it needs. The runner stage sets CLOUDFLARE_ADAPTER=false on the web image and runs both as a non-root user (server for the API, astro for the web, uid/gid 1001) for a smaller attack surface.
Environment variables
Every variable Feeblo understands, grouped and explained.
Database
PostgreSQL setup and how migrations run automatically in the image.
Media storage
Point Feeblo at MinIO, R2, or any S3-compatible store.
SMTP transport configuration for transactional email.
Self-hosting overview
How a self-hosted Feeblo deployment fits together — the two images, the external services, and the environment that wires them up.
Environment variables
Every environment variable Feeblo understands — what each one does, which app reads it, and the defaults — grouped by section, sourced from .reference/.env.example and the Effect configs.